Compare commits

...

49 Commits

Author SHA1 Message Date
henrygd
6e3fd90834 0.18.7 release 2026-04-05 16:14:39 -04:00
henrygd
5ab82183fa comment out unneeded test that returns different status in dev / prod (add todo) 2026-04-05 16:14:28 -04:00
henrygd
a68e02ca84 update translations 2026-04-05 15:22:26 -04:00
henrygd
0f2e16c63c deps: upgrade pocketbase 2026-04-04 18:31:33 -04:00
Sven van Ginkel
c4009f2b43 feat: add more disk I/O metrics (#1866)
Co-authored-by: henrygd <hank@henrygd.me>
2026-04-04 18:28:05 -04:00
Sven van Ginkel
ef0c1420d1 fix(deps): resolve 9 npm Dependabot security alerts (#1882) 2026-04-03 13:23:23 -04:00
henrygd
eb9a8e1ef9 install: update opnsense detection path (#1880) 2026-04-03 12:16:27 -04:00
henrygd
6b5e6ffa9a agent: small refactoring and tests for battery package (#1872) 2026-04-02 21:07:14 -04:00
henrygd
d656036d3b agent: refactor new battery package (#1872) 2026-04-02 21:07:14 -04:00
svenvg93
80b73c7faf feat: implement the battery diectly instead of depency 2026-04-02 21:07:14 -04:00
Sven van Ginkel
afe9eb7a70 feat(hub): copy existing alerts between systems (#1853)
Co-authored-by: henrygd <hank@henrygd.me>
2026-04-02 18:04:45 -04:00
Sven van Ginkel
7f565a3086 fix(agent): show correct NVMe capacity for Apple SSDs (#1873)
Co-authored-by: henrygd <hank@henrygd.me>
2026-04-02 15:36:05 -04:00
Sven van Ginkel
77862d4cb1 fix(install): use daemon user on OPNsense to survive reboots#1880 2026-04-02 15:34:50 -04:00
Sven van Ginkel
e158a9001b fix(agent): upgrade gopsutil to v4.26.3 to resolve macOS ARM64 crashes (#1881, #796) 2026-04-02 15:23:08 -04:00
henrygd
f670e868e4 agent: add SENSORS_TIMEOUT env var (#1871) 2026-04-02 15:10:49 -04:00
henrygd
0fff699bf6 hub: return error if accessing /api/beszel/universal-token with a superuser account (#1870) 2026-04-01 22:16:47 -04:00
henrygd
ba10da1b9f hub: add additional validation checks for custom api routes
- Validate the user is assigned to system in authenticated routes where
the user passes in system ID. This protects against a somewhat
impractical scenario where an authenticated user cracks a random 15
character alphanumeric ID of a system that doesn't belong to them via
web API.
- Validate that systemd service exists in database before requesting
service details from agent. This protects against authenticated users
getting unit properties of services that aren't explicitly monitored.
- Refactor responses in authenticated routes to prevent enumeration of
other users' random 15 char system IDs.
2026-04-01 16:30:45 -04:00
henrygd
7f4f14b505 fix(agent,windows): raise timeout on first sensor collection to allow LHM to start 2026-03-31 16:10:59 -04:00
henrygd
2fda4ff264 agent: update LibreHardwareMonitorLib to 0.9.6 2026-03-31 15:55:02 -04:00
henrygd
20b0b40ec8 ui: no centered dialog headings and a few other tweaks 2026-03-31 15:40:52 -04:00
Malith Rukshan
d548a012b4 fix(ui): revert CardTitle to text-2xl to fix tailwind-merge class override (#1860) 2026-03-31 14:55:23 -04:00
henrygd
ce5d1217dd fix(hub): cancel pending down status alert if system paused before alert sent 2026-03-31 14:08:44 -04:00
henrygd
cef09d7cb1 fix(agent): fix windows root disk detection if exe not running on root disk (#1863) 2026-03-31 12:58:42 -04:00
henrygd
f6440acb43 fix(ui): hide noop add system btn and smart actions for readonly users 2026-03-30 19:45:12 -04:00
henrygd
5463a38f0f refactor(hub): move api user role checks to middlewares 2026-03-30 19:35:02 -04:00
Sven van Ginkel
80135fdad3 fix(agent): exclude nested virtual fs when mounting host root to /extra-filesystems in Docker (#1859) 2026-03-30 13:48:54 -04:00
henrygd
5db4eb4346 0.18.6 release 2026-03-29 13:03:48 -04:00
Yi Zhou
f6c5e2928a Add apple-touch-icon link to index.html (#1850) 2026-03-29 12:39:38 -04:00
henrygd
6a207c33fa agent: change disk.Partitions(false) to true - likely fixes empty partition list in docker as of gopsutil 4.26.2 2026-03-29 12:33:45 -04:00
henrygd
9f19afccde hub: reset smart interval on agent reconnect if agent hasn't successfully saved smart devices
this is so people trying to get smart working can see the config changes immediately. not need to wait for the smart interval.
2026-03-29 12:30:39 -04:00
henrygd
f25f2469e3 hub: add debug logs for smart behavior (#1800) 2026-03-28 21:16:26 -04:00
henrygd
5bd43ed461 hub: reset smart interval on agent reconnect if agent hasn't successfully saved smart devices
this is so people trying to get smart working can see the config changes immediately. not need to wait for the smart interval.
2026-03-28 20:47:16 -04:00
henrygd
afdc3f7779 fix(agent): allow GPU_COLLECTOR=nvml without nvidia-smi (#1849) 2026-03-28 18:58:16 -04:00
henrygd
a227c77526 agent: detect podman correctly when using socket proxy (#1846) 2026-03-28 17:43:29 -04:00
henrygd
8202d746af fix(hub): ui bug where charts didn't display 1m max until next update 2026-03-28 12:16:12 -04:00
henrygd
9840b99327 0.18.5 release 2026-03-27 16:27:53 -04:00
henrygd
f7b5a505e8 update translations 2026-03-27 15:57:26 -04:00
henrygd
3cb32ac046 hub(ui): add spacing at bottom of the page if temp tooltip is very long 2026-03-27 14:54:31 -04:00
henrygd
e610d9bfc8 ui: standardize table styles 2026-03-27 14:08:59 -04:00
henrygd
b53fdbe0ef fix(agent): find macmon if /opt/homebrew/bin is not in path (#1746) 2026-03-27 13:52:22 -04:00
henrygd
c7261b56f1 hub(ui): style cleanup and mobile improvements 2026-03-27 12:26:00 -04:00
henrygd
3f4c3d51b6 update go deps 2026-03-27 12:25:17 -04:00
Jim Haff
ad21cab457 Prevent temperature collection from blocking agent stats (#1839) 2026-03-26 20:03:51 -04:00
henrygd
f04684b30a hub(ui): small js optimizations 2026-03-26 19:16:39 -04:00
Stavros
4d4e4fba9b feat: use dropdown menu as navigation on mobile devices (#1840)
Co-authored-by: henrygd <hank@henrygd.me>
2026-03-26 18:27:42 -04:00
henrygd
62587919f4 hub(ui): tabs display for system + major frontend/charts refactoring
- System page tabs display option
- Remove very specific chart components (disk usage, container cpu, etc)
and refactor to use more flexible area and line chart components
- Optimizations around chart handling to decrease mem usage. Charts are
only redrawn now if in view.
- Resolve most of the react dev warnings

Co-authored-by: sveng93 <svenvanginkel@icloud.com>
2026-03-26 15:21:39 -04:00
henrygd
35528332fd hub: fix possible nil pointer panic in realtime worker 2026-03-26 10:48:37 -04:00
henrygd
e3e453140e fix(agent): isolate container network rate tracking per cache interval
Previously, the agent shared a single PrevReadTime timestamp across all
collection intervals (e.g., 1s and 60s). This caused the 60s collector
to divide its accumulated 60s byte delta by the tiny time elapsed since
the last 1s collection, resulting in astronomically inflated network
rates. The fix introduces per-cache-time read time tracking, ensuring
calculations for each interval use their own independent timing context.
2026-03-24 13:07:56 -04:00
henrygd
7a64da9f65 hub: add guard to WSConn.Ping to ensure no nil conn ptr 2026-03-23 15:25:43 -04:00
125 changed files with 15327 additions and 5478 deletions

View File

@@ -19,6 +19,8 @@ import (
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
const defaultDataCacheTimeMs uint16 = 60_000
type Agent struct { type Agent struct {
sync.Mutex // Used to lock agent while collecting data sync.Mutex // Used to lock agent while collecting data
debug bool // true if LOG_LEVEL is set to debug debug bool // true if LOG_LEVEL is set to debug
@@ -36,6 +38,7 @@ type Agent struct {
sensorConfig *SensorConfig // Sensors config sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info (dynamic) systemInfo system.Info // Host system info (dynamic)
systemDetails system.Details // Host system details (static, once-per-connection) systemDetails system.Details // Host system details (static, once-per-connection)
detailsDirty bool // Whether system details have changed and need to be resent
gpuManager *GPUManager // Manages GPU data gpuManager *GPUManager // Manages GPU data
cache *systemDataCache // Cache for system stats based on cache time cache *systemDataCache // Cache for system stats based on cache time
connectionManager *ConnectionManager // Channel to signal connection events connectionManager *ConnectionManager // Channel to signal connection events
@@ -97,7 +100,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
slog.Debug(beszel.Version) slog.Debug(beszel.Version)
// initialize docker manager // initialize docker manager
agent.dockerManager = newDockerManager() agent.dockerManager = newDockerManager(agent)
// initialize system info // initialize system info
agent.refreshSystemDetails() agent.refreshSystemDetails()
@@ -142,7 +145,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// if debugging, print stats // if debugging, print stats
if agent.debug { if agent.debug {
slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000, IncludeDetails: true})) slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs, IncludeDetails: true}))
} }
return agent, nil return agent, nil
@@ -164,11 +167,6 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD
Info: a.systemInfo, Info: a.systemInfo,
} }
// Include static system details only when requested
if options.IncludeDetails {
data.Details = &a.systemDetails
}
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs) // slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
if a.dockerManager != nil { if a.dockerManager != nil {
@@ -181,7 +179,7 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD
} }
// skip updating systemd services if cache time is not the default 60sec interval // skip updating systemd services if cache time is not the default 60sec interval
if a.systemdManager != nil && cacheTimeMs == 60_000 { if a.systemdManager != nil && cacheTimeMs == defaultDataCacheTimeMs {
totalCount := uint16(a.systemdManager.getServiceStatsCount()) totalCount := uint16(a.systemdManager.getServiceStatsCount())
if totalCount > 0 { if totalCount > 0 {
numFailed := a.systemdManager.getFailedServiceCount() numFailed := a.systemdManager.getFailedServiceCount()
@@ -212,7 +210,8 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD
slog.Debug("Extra FS", "data", data.Stats.ExtraFs) slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
a.cache.Set(data, cacheTimeMs) a.cache.Set(data, cacheTimeMs)
return data
return a.attachSystemDetails(data, cacheTimeMs, options.IncludeDetails)
} }
// Start initializes and starts the agent with optional WebSocket connection // Start initializes and starts the agent with optional WebSocket connection

View File

@@ -1,84 +1,11 @@
//go:build !freebsd // Package battery provides functions to check if the system has a battery and return the charge state and percentage.
// Package battery provides functions to check if the system has a battery and to get the battery stats.
package battery package battery
import ( const (
"errors" stateUnknown uint8 = iota
"log/slog" stateEmpty
"math" stateFull
stateCharging
"github.com/distatus/battery" stateDischarging
stateIdle
) )
var (
systemHasBattery = false
haveCheckedBattery = false
)
// HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool {
if haveCheckedBattery {
return systemHasBattery
}
haveCheckedBattery = true
batteries, err := battery.GetAll()
for _, bat := range batteries {
if bat != nil && (bat.Full > 0 || bat.Design > 0) {
systemHasBattery = true
break
}
}
if !systemHasBattery {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery
}
// GetBatteryStats returns the current battery percent and charge state
// percent = (current charge of all batteries) / (sum of designed/full capacity of all batteries)
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !HasReadableBattery() {
return batteryPercent, batteryState, errors.ErrUnsupported
}
batteries, err := battery.GetAll()
// we'll handle errors later by skipping batteries with errors, rather
// than skipping everything because of the presence of some errors.
if len(batteries) == 0 {
return batteryPercent, batteryState, errors.New("no batteries")
}
totalCapacity := float64(0)
totalCharge := float64(0)
errs, partialErrs := err.(battery.Errors)
batteryState = math.MaxUint8
for i, bat := range batteries {
if partialErrs && errs[i] != nil {
// if there were some errors, like missing data, skip it
continue
}
if bat == nil || bat.Full == 0 {
// skip batteries with no capacity. Charge is unlikely to ever be zero, but
// we can't guarantee that, so don't skip based on charge.
continue
}
totalCapacity += bat.Full
totalCharge += min(bat.Current, bat.Full)
if bat.State.Raw >= 0 {
batteryState = uint8(bat.State.Raw)
}
}
if totalCapacity == 0 || batteryState == math.MaxUint8 {
// for macs there's sometimes a ghost battery with 0 capacity
// https://github.com/distatus/battery/issues/34
// Instead of skipping over those batteries, we'll check for total 0 capacity
// and return an error. This also prevents a divide by zero.
return batteryPercent, batteryState, errors.New("no battery capacity")
}
batteryPercent = uint8(totalCharge / totalCapacity * 100)
return batteryPercent, batteryState, nil
}

View File

@@ -0,0 +1,96 @@
//go:build darwin
package battery
import (
"errors"
"log/slog"
"math"
"os/exec"
"sync"
"howett.net/plist"
)
type macBattery struct {
CurrentCapacity int `plist:"CurrentCapacity"`
MaxCapacity int `plist:"MaxCapacity"`
FullyCharged bool `plist:"FullyCharged"`
IsCharging bool `plist:"IsCharging"`
ExternalConnected bool `plist:"ExternalConnected"`
}
func readMacBatteries() ([]macBattery, error) {
out, err := exec.Command("ioreg", "-n", "AppleSmartBattery", "-r", "-a").Output()
if err != nil {
return nil, err
}
if len(out) == 0 {
return nil, nil
}
var batteries []macBattery
if _, err := plist.Unmarshal(out, &batteries); err != nil {
return nil, err
}
return batteries, nil
}
// HasReadableBattery checks if the system has a battery and returns true if it does.
var HasReadableBattery = sync.OnceValue(func() bool {
systemHasBattery := false
batteries, err := readMacBatteries()
slog.Debug("Batteries", "batteries", batteries, "err", err)
for _, bat := range batteries {
if bat.MaxCapacity > 0 {
systemHasBattery = true
break
}
}
return systemHasBattery
})
// GetBatteryStats returns the current battery percent and charge state.
// Uses CurrentCapacity/MaxCapacity to match the value macOS displays.
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !HasReadableBattery() {
return batteryPercent, batteryState, errors.ErrUnsupported
}
batteries, err := readMacBatteries()
if len(batteries) == 0 {
return batteryPercent, batteryState, errors.New("no batteries")
}
totalCapacity := 0
totalCharge := 0
batteryState = math.MaxUint8
for _, bat := range batteries {
if bat.MaxCapacity == 0 {
// skip ghost batteries with 0 capacity
// https://github.com/distatus/battery/issues/34
continue
}
totalCapacity += bat.MaxCapacity
totalCharge += min(bat.CurrentCapacity, bat.MaxCapacity)
switch {
case !bat.ExternalConnected:
batteryState = stateDischarging
case bat.IsCharging:
batteryState = stateCharging
case bat.CurrentCapacity == 0:
batteryState = stateEmpty
case !bat.FullyCharged:
batteryState = stateIdle
default:
batteryState = stateFull
}
}
if totalCapacity == 0 || batteryState == math.MaxUint8 {
return batteryPercent, batteryState, errors.New("no battery capacity")
}
batteryPercent = uint8(float64(totalCharge) / float64(totalCapacity) * 100)
return batteryPercent, batteryState, nil
}

View File

@@ -0,0 +1,117 @@
//go:build linux
package battery
import (
"errors"
"log/slog"
"math"
"os"
"path/filepath"
"strconv"
"sync"
"github.com/henrygd/beszel/agent/utils"
)
// getBatteryPaths returns the paths of all batteries in /sys/class/power_supply
var getBatteryPaths func() ([]string, error)
// HasReadableBattery checks if the system has a battery and returns true if it does.
var HasReadableBattery func() bool
func init() {
resetBatteryState("/sys/class/power_supply")
}
// resetBatteryState resets the sync.Once functions to a fresh state.
// Tests call this after swapping sysfsPowerSupply so the new path is picked up.
func resetBatteryState(sysfsPowerSupplyPath string) {
getBatteryPaths = sync.OnceValues(func() ([]string, error) {
entries, err := os.ReadDir(sysfsPowerSupplyPath)
if err != nil {
return nil, err
}
var paths []string
for _, e := range entries {
path := filepath.Join(sysfsPowerSupplyPath, e.Name())
if utils.ReadStringFile(filepath.Join(path, "type")) == "Battery" {
paths = append(paths, path)
}
}
return paths, nil
})
HasReadableBattery = sync.OnceValue(func() bool {
systemHasBattery := false
paths, err := getBatteryPaths()
for _, path := range paths {
if _, ok := utils.ReadStringFileOK(filepath.Join(path, "capacity")); ok {
systemHasBattery = true
break
}
}
if !systemHasBattery {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery
})
}
func parseSysfsState(status string) uint8 {
switch status {
case "Empty":
return stateEmpty
case "Full":
return stateFull
case "Charging":
return stateCharging
case "Discharging":
return stateDischarging
case "Not charging":
return stateIdle
default:
return stateUnknown
}
}
// GetBatteryStats returns the current battery percent and charge state.
// Reads /sys/class/power_supply/*/capacity directly so the kernel-reported
// value is used, which is always 0-100 and matches what the OS displays.
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !HasReadableBattery() {
return batteryPercent, batteryState, errors.ErrUnsupported
}
paths, err := getBatteryPaths()
if len(paths) == 0 {
return batteryPercent, batteryState, errors.New("no batteries")
}
batteryState = math.MaxUint8
totalPercent := 0
count := 0
for _, path := range paths {
capStr, ok := utils.ReadStringFileOK(filepath.Join(path, "capacity"))
if !ok {
continue
}
cap, parseErr := strconv.Atoi(capStr)
if parseErr != nil {
continue
}
totalPercent += cap
count++
state := parseSysfsState(utils.ReadStringFile(filepath.Join(path, "status")))
if state != stateUnknown {
batteryState = state
}
}
if count == 0 || batteryState == math.MaxUint8 {
return batteryPercent, batteryState, errors.New("no battery capacity")
}
batteryPercent = uint8(totalPercent / count)
return batteryPercent, batteryState, nil
}

View File

@@ -0,0 +1,201 @@
//go:build testing && linux
package battery
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
// setupFakeSysfs creates a temporary sysfs-like tree under t.TempDir(),
// swaps sysfsPowerSupply, resets the sync.Once caches, and restores
// everything on cleanup. Returns a helper to create battery directories.
func setupFakeSysfs(t *testing.T) (tmpDir string, addBattery func(name, capacity, status string)) {
t.Helper()
tmp := t.TempDir()
resetBatteryState(tmp)
write := func(path, content string) {
t.Helper()
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
addBattery = func(name, capacity, status string) {
t.Helper()
batDir := filepath.Join(tmp, name)
write(filepath.Join(batDir, "type"), "Battery")
write(filepath.Join(batDir, "capacity"), capacity)
write(filepath.Join(batDir, "status"), status)
}
return tmp, addBattery
}
func TestParseSysfsState(t *testing.T) {
tests := []struct {
input string
want uint8
}{
{"Empty", stateEmpty},
{"Full", stateFull},
{"Charging", stateCharging},
{"Discharging", stateDischarging},
{"Not charging", stateIdle},
{"", stateUnknown},
{"SomethingElse", stateUnknown},
}
for _, tt := range tests {
assert.Equal(t, tt.want, parseSysfsState(tt.input), "parseSysfsState(%q)", tt.input)
}
}
func TestGetBatteryStats_SingleBattery(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "72", "Discharging")
pct, state, err := GetBatteryStats()
assert.NoError(t, err)
assert.Equal(t, uint8(72), pct)
assert.Equal(t, stateDischarging, state)
}
func TestGetBatteryStats_MultipleBatteries(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "80", "Charging")
addBattery("BAT1", "40", "Charging")
pct, state, err := GetBatteryStats()
assert.NoError(t, err)
// average of 80 and 40 = 60
assert.EqualValues(t, 60, pct)
assert.Equal(t, stateCharging, state)
}
func TestGetBatteryStats_FullBattery(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "100", "Full")
pct, state, err := GetBatteryStats()
assert.NoError(t, err)
assert.Equal(t, uint8(100), pct)
assert.Equal(t, stateFull, state)
}
func TestGetBatteryStats_EmptyBattery(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "0", "Empty")
pct, state, err := GetBatteryStats()
assert.NoError(t, err)
assert.Equal(t, uint8(0), pct)
assert.Equal(t, stateEmpty, state)
}
func TestGetBatteryStats_NotCharging(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "80", "Not charging")
pct, state, err := GetBatteryStats()
assert.NoError(t, err)
assert.Equal(t, uint8(80), pct)
assert.Equal(t, stateIdle, state)
}
func TestGetBatteryStats_NoBatteries(t *testing.T) {
setupFakeSysfs(t) // empty directory, no batteries
_, _, err := GetBatteryStats()
assert.Error(t, err)
}
func TestGetBatteryStats_NonBatterySupplyIgnored(t *testing.T) {
tmp, addBattery := setupFakeSysfs(t)
// Add a real battery
addBattery("BAT0", "55", "Charging")
// Add an AC adapter (type != Battery) - should be ignored
acDir := filepath.Join(tmp, "AC0")
if err := os.MkdirAll(acDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(acDir, "type"), []byte("Mains"), 0o644); err != nil {
t.Fatal(err)
}
pct, state, err := GetBatteryStats()
assert.NoError(t, err)
assert.Equal(t, uint8(55), pct)
assert.Equal(t, stateCharging, state)
}
func TestGetBatteryStats_InvalidCapacitySkipped(t *testing.T) {
tmp, addBattery := setupFakeSysfs(t)
// One battery with valid capacity
addBattery("BAT0", "90", "Discharging")
// Another with invalid capacity text
badDir := filepath.Join(tmp, "BAT1")
if err := os.MkdirAll(badDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(badDir, "type"), []byte("Battery"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(badDir, "capacity"), []byte("not-a-number"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(badDir, "status"), []byte("Discharging"), 0o644); err != nil {
t.Fatal(err)
}
pct, _, err := GetBatteryStats()
assert.NoError(t, err)
// Only BAT0 counted
assert.Equal(t, uint8(90), pct)
}
func TestGetBatteryStats_UnknownStatusOnly(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "50", "SomethingWeird")
_, _, err := GetBatteryStats()
assert.Error(t, err)
}
func TestHasReadableBattery_True(t *testing.T) {
_, addBattery := setupFakeSysfs(t)
addBattery("BAT0", "50", "Charging")
assert.True(t, HasReadableBattery())
}
func TestHasReadableBattery_False(t *testing.T) {
setupFakeSysfs(t) // no batteries
assert.False(t, HasReadableBattery())
}
func TestHasReadableBattery_NoCapacityFile(t *testing.T) {
tmp, _ := setupFakeSysfs(t)
// Battery dir with type file but no capacity file
batDir := filepath.Join(tmp, "BAT0")
err := os.MkdirAll(batDir, 0o755)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(batDir, "type"), []byte("Battery"), 0o644)
assert.NoError(t, err)
assert.False(t, HasReadableBattery())
}

View File

@@ -1,4 +1,4 @@
//go:build freebsd //go:build !darwin && !linux && !windows
package battery package battery

View File

@@ -0,0 +1,298 @@
//go:build windows
// Most of the Windows battery code is based on
// distatus/battery by Karol 'Kenji Takahashi' Woźniak
package battery
import (
"errors"
"log/slog"
"math"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type batteryQueryInformation struct {
BatteryTag uint32
InformationLevel int32
AtRate int32
}
type batteryInformation struct {
Capabilities uint32
Technology uint8
Reserved [3]uint8
Chemistry [4]uint8
DesignedCapacity uint32
FullChargedCapacity uint32
DefaultAlert1 uint32
DefaultAlert2 uint32
CriticalBias uint32
CycleCount uint32
}
type batteryWaitStatus struct {
BatteryTag uint32
Timeout uint32
PowerState uint32
LowCapacity uint32
HighCapacity uint32
}
type batteryStatus struct {
PowerState uint32
Capacity uint32
Voltage uint32
Rate int32
}
type winGUID struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
type spDeviceInterfaceData struct {
cbSize uint32
InterfaceClassGuid winGUID
Flags uint32
Reserved uint
}
var guidDeviceBattery = winGUID{
0x72631e54,
0x78A4,
0x11d0,
[8]byte{0xbc, 0xf7, 0x00, 0xaa, 0x00, 0xb7, 0xb3, 0x2a},
}
var (
setupapi = &windows.LazyDLL{Name: "setupapi.dll", System: true}
setupDiGetClassDevsW = setupapi.NewProc("SetupDiGetClassDevsW")
setupDiEnumDeviceInterfaces = setupapi.NewProc("SetupDiEnumDeviceInterfaces")
setupDiGetDeviceInterfaceDetailW = setupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
setupDiDestroyDeviceInfoList = setupapi.NewProc("SetupDiDestroyDeviceInfoList")
)
// winBatteryGet reads one battery by index. Returns (fullCapacity, currentCapacity, state, error).
// Returns error == errNotFound when there are no more batteries.
var errNotFound = errors.New("no more batteries")
func setupDiSetup(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) (uintptr, error) {
_ = nargs
r1, _, errno := syscall.SyscallN(proc.Addr(), a1, a2, a3, a4, a5, a6)
if windows.Handle(r1) == windows.InvalidHandle {
if errno != 0 {
return 0, error(errno)
}
return 0, syscall.EINVAL
}
return r1, nil
}
func setupDiCall(proc *windows.LazyProc, nargs, a1, a2, a3, a4, a5, a6 uintptr) syscall.Errno {
_ = nargs
r1, _, errno := syscall.SyscallN(proc.Addr(), a1, a2, a3, a4, a5, a6)
if r1 == 0 {
if errno != 0 {
return errno
}
return syscall.EINVAL
}
return 0
}
func readWinBatteryState(powerState uint32) uint8 {
switch {
case powerState&0x00000004 != 0:
return stateCharging
case powerState&0x00000008 != 0:
return stateEmpty
case powerState&0x00000002 != 0:
return stateDischarging
case powerState&0x00000001 != 0:
return stateFull
default:
return stateUnknown
}
}
func winBatteryGet(idx int) (full, current uint32, state uint8, err error) {
hdev, err := setupDiSetup(
setupDiGetClassDevsW,
4,
uintptr(unsafe.Pointer(&guidDeviceBattery)),
0, 0,
2|16, // DIGCF_PRESENT|DIGCF_DEVICEINTERFACE
0, 0,
)
if err != nil {
return 0, 0, stateUnknown, err
}
defer syscall.SyscallN(setupDiDestroyDeviceInfoList.Addr(), hdev)
var did spDeviceInterfaceData
did.cbSize = uint32(unsafe.Sizeof(did))
errno := setupDiCall(
setupDiEnumDeviceInterfaces,
5,
hdev, 0,
uintptr(unsafe.Pointer(&guidDeviceBattery)),
uintptr(idx),
uintptr(unsafe.Pointer(&did)),
0,
)
if errno == 259 { // ERROR_NO_MORE_ITEMS
return 0, 0, stateUnknown, errNotFound
}
if errno != 0 {
return 0, 0, stateUnknown, errno
}
var cbRequired uint32
errno = setupDiCall(
setupDiGetDeviceInterfaceDetailW,
6,
hdev,
uintptr(unsafe.Pointer(&did)),
0, 0,
uintptr(unsafe.Pointer(&cbRequired)),
0,
)
if errno != 0 && errno != 122 { // ERROR_INSUFFICIENT_BUFFER
return 0, 0, stateUnknown, errno
}
didd := make([]uint16, cbRequired/2)
cbSize := (*uint32)(unsafe.Pointer(&didd[0]))
if unsafe.Sizeof(uint(0)) == 8 {
*cbSize = 8
} else {
*cbSize = 6
}
errno = setupDiCall(
setupDiGetDeviceInterfaceDetailW,
6,
hdev,
uintptr(unsafe.Pointer(&did)),
uintptr(unsafe.Pointer(&didd[0])),
uintptr(cbRequired),
uintptr(unsafe.Pointer(&cbRequired)),
0,
)
if errno != 0 {
return 0, 0, stateUnknown, errno
}
devicePath := &didd[2:][0]
handle, err := windows.CreateFile(
devicePath,
windows.GENERIC_READ|windows.GENERIC_WRITE,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return 0, 0, stateUnknown, err
}
defer windows.CloseHandle(handle)
var dwOut uint32
var dwWait uint32
var bqi batteryQueryInformation
err = windows.DeviceIoControl(
handle,
2703424, // IOCTL_BATTERY_QUERY_TAG
(*byte)(unsafe.Pointer(&dwWait)),
uint32(unsafe.Sizeof(dwWait)),
(*byte)(unsafe.Pointer(&bqi.BatteryTag)),
uint32(unsafe.Sizeof(bqi.BatteryTag)),
&dwOut, nil,
)
if err != nil || bqi.BatteryTag == 0 {
return 0, 0, stateUnknown, errors.New("battery tag not returned")
}
var bi batteryInformation
if err = windows.DeviceIoControl(
handle,
2703428, // IOCTL_BATTERY_QUERY_INFORMATION
(*byte)(unsafe.Pointer(&bqi)),
uint32(unsafe.Sizeof(bqi)),
(*byte)(unsafe.Pointer(&bi)),
uint32(unsafe.Sizeof(bi)),
&dwOut, nil,
); err != nil {
return 0, 0, stateUnknown, err
}
bws := batteryWaitStatus{BatteryTag: bqi.BatteryTag}
var bs batteryStatus
if err = windows.DeviceIoControl(
handle,
2703436, // IOCTL_BATTERY_QUERY_STATUS
(*byte)(unsafe.Pointer(&bws)),
uint32(unsafe.Sizeof(bws)),
(*byte)(unsafe.Pointer(&bs)),
uint32(unsafe.Sizeof(bs)),
&dwOut, nil,
); err != nil {
return 0, 0, stateUnknown, err
}
if bs.Capacity == 0xffffffff { // BATTERY_UNKNOWN_CAPACITY
return 0, 0, stateUnknown, errors.New("battery capacity unknown")
}
return bi.FullChargedCapacity, bs.Capacity, readWinBatteryState(bs.PowerState), nil
}
// HasReadableBattery checks if the system has a battery and returns true if it does.
var HasReadableBattery = sync.OnceValue(func() bool {
systemHasBattery := false
full, _, _, err := winBatteryGet(0)
if err == nil && full > 0 {
systemHasBattery = true
}
if !systemHasBattery {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery
})
// GetBatteryStats returns the current battery percent and charge state.
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !HasReadableBattery() {
return batteryPercent, batteryState, errors.ErrUnsupported
}
totalFull := uint32(0)
totalCurrent := uint32(0)
batteryState = math.MaxUint8
for i := 0; ; i++ {
full, current, state, bErr := winBatteryGet(i)
if errors.Is(bErr, errNotFound) {
break
}
if bErr != nil || full == 0 {
continue
}
totalFull += full
totalCurrent += min(current, full)
batteryState = state
}
if totalFull == 0 || batteryState == math.MaxUint8 {
return batteryPercent, batteryState, errors.New("no battery capacity")
}
batteryPercent = uint8(float64(totalCurrent) / float64(totalFull) * 100)
return batteryPercent, batteryState, nil
}

View File

@@ -1,6 +1,7 @@
package agent package agent
import ( import (
"context"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@@ -33,6 +34,34 @@ type diskDiscovery struct {
ctx fsRegistrationContext ctx fsRegistrationContext
} }
// prevDisk stores previous per-device disk counters for a given cache interval
type prevDisk struct {
readBytes uint64
writeBytes uint64
readTime uint64 // cumulative ms spent on reads (from ReadTime)
writeTime uint64 // cumulative ms spent on writes (from WriteTime)
ioTime uint64 // cumulative ms spent doing I/O (from IoTime)
weightedIO uint64 // cumulative weighted ms (queue-depth × ms, from WeightedIO)
readCount uint64 // cumulative read operation count
writeCount uint64 // cumulative write operation count
at time.Time
}
// prevDiskFromCounter creates a prevDisk snapshot from a disk.IOCountersStat at time t.
func prevDiskFromCounter(d disk.IOCountersStat, t time.Time) prevDisk {
return prevDisk{
readBytes: d.ReadBytes,
writeBytes: d.WriteBytes,
readTime: d.ReadTime,
writeTime: d.WriteTime,
ioTime: d.IoTime,
weightedIO: d.WeightedIO,
readCount: d.ReadCount,
writeCount: d.WriteCount,
at: t,
}
}
// parseFilesystemEntry parses a filesystem entry in the format "device__customname" // parseFilesystemEntry parses a filesystem entry in the format "device__customname"
// Returns the device/filesystem part and the custom name part // Returns the device/filesystem part and the custom name part
func parseFilesystemEntry(entry string) (device, customName string) { func parseFilesystemEntry(entry string) (device, customName string) {
@@ -238,9 +267,11 @@ func (d *diskDiscovery) addConfiguredExtraFilesystems(extraFilesystems string) {
// addPartitionExtraFs registers partitions mounted under /extra-filesystems so // addPartitionExtraFs registers partitions mounted under /extra-filesystems so
// their display names can come from the folder name while their I/O keys still // their display names can come from the folder name while their I/O keys still
// prefer the underlying partition device. // prefer the underlying partition device. Only direct children are matched to
// avoid registering nested virtual mounts (e.g. /proc, /sys) that are returned by
// disk.Partitions(true) when the host root is bind-mounted in /extra-filesystems.
func (d *diskDiscovery) addPartitionExtraFs(p disk.PartitionStat) { func (d *diskDiscovery) addPartitionExtraFs(p disk.PartitionStat) {
if !strings.HasPrefix(p.Mountpoint, d.ctx.efPath) { if filepath.Dir(p.Mountpoint) != d.ctx.efPath {
return return
} }
device, customName := extraFilesystemPartitionInfo(p) device, customName := extraFilesystemPartitionInfo(p)
@@ -273,7 +304,7 @@ func (a *Agent) initializeDiskInfo() {
hasRoot := false hasRoot := false
isWindows := runtime.GOOS == "windows" isWindows := runtime.GOOS == "windows"
partitions, err := disk.Partitions(false) partitions, err := disk.PartitionsWithContext(context.Background(), true)
if err != nil { if err != nil {
slog.Error("Error getting disk partitions", "err", err) slog.Error("Error getting disk partitions", "err", err)
} }
@@ -578,16 +609,29 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
prev, hasPrev := a.diskPrev[cacheTimeMs][name] prev, hasPrev := a.diskPrev[cacheTimeMs][name]
if !hasPrev { if !hasPrev {
// Seed from agent-level fsStats if present, else seed from current // Seed from agent-level fsStats if present, else seed from current
prev = prevDisk{readBytes: stats.TotalRead, writeBytes: stats.TotalWrite, at: stats.Time} prev = prevDisk{
readBytes: stats.TotalRead,
writeBytes: stats.TotalWrite,
readTime: d.ReadTime,
writeTime: d.WriteTime,
ioTime: d.IoTime,
weightedIO: d.WeightedIO,
readCount: d.ReadCount,
writeCount: d.WriteCount,
at: stats.Time,
}
if prev.at.IsZero() { if prev.at.IsZero() {
prev = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now} prev = prevDiskFromCounter(d, now)
} }
} }
msElapsed := uint64(now.Sub(prev.at).Milliseconds()) msElapsed := uint64(now.Sub(prev.at).Milliseconds())
// Update per-interval snapshot
a.diskPrev[cacheTimeMs][name] = prevDiskFromCounter(d, now)
// Avoid division by zero or clock issues
if msElapsed < 100 { if msElapsed < 100 {
// Avoid division by zero or clock issues; update snapshot and continue
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
continue continue
} }
@@ -599,15 +643,38 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
// validate values // validate values
if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 { if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {
slog.Warn("Invalid disk I/O. Resetting.", "name", d.Name, "read", readMbPerSecond, "write", writeMbPerSecond) slog.Warn("Invalid disk I/O. Resetting.", "name", d.Name, "read", readMbPerSecond, "write", writeMbPerSecond)
// Reset interval snapshot and seed from current
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
// also refresh agent baseline to avoid future negatives // also refresh agent baseline to avoid future negatives
a.initializeDiskIoStats(ioCounters) a.initializeDiskIoStats(ioCounters)
continue continue
} }
// Update per-interval snapshot // These properties are calculated differently on different platforms,
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now} // but generally represent cumulative time spent doing reads/writes on the device.
// This can surpass 100% if there are multiple concurrent I/O operations.
// Linux kernel docs:
// This is the total number of milliseconds spent by all reads (as
// measured from __make_request() to end_that_request_last()).
// https://www.kernel.org/doc/Documentation/iostats.txt (fields 4, 8)
diskReadTime := utils.TwoDecimals(float64(d.ReadTime-prev.readTime) / float64(msElapsed) * 100)
diskWriteTime := utils.TwoDecimals(float64(d.WriteTime-prev.writeTime) / float64(msElapsed) * 100)
// I/O utilization %: fraction of wall time the device had any I/O in progress (0-100).
diskIoUtilPct := utils.TwoDecimals(float64(d.IoTime-prev.ioTime) / float64(msElapsed) * 100)
// Weighted I/O: queue-depth weighted I/O time, normalized to interval (can exceed 100%).
// Linux kernel field 11: incremented by iops_in_progress × ms_since_last_update.
// Used to display queue depth. Multipled by 100 to increase accuracy of digit truncation (divided by 100 in UI).
diskWeightedIO := utils.TwoDecimals(float64(d.WeightedIO-prev.weightedIO) / float64(msElapsed) * 100)
// r_await / w_await: average time per read/write operation in milliseconds.
// Equivalent to r_await and w_await in iostat.
var rAwait, wAwait float64
if deltaReadCount := d.ReadCount - prev.readCount; deltaReadCount > 0 {
rAwait = utils.TwoDecimals(float64(d.ReadTime-prev.readTime) / float64(deltaReadCount))
}
if deltaWriteCount := d.WriteCount - prev.writeCount; deltaWriteCount > 0 {
wAwait = utils.TwoDecimals(float64(d.WriteTime-prev.writeTime) / float64(deltaWriteCount))
}
// Update global fsStats baseline for cross-interval correctness // Update global fsStats baseline for cross-interval correctness
stats.Time = now stats.Time = now
@@ -617,20 +684,40 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
stats.DiskWritePs = writeMbPerSecond stats.DiskWritePs = writeMbPerSecond
stats.DiskReadBytes = diskIORead stats.DiskReadBytes = diskIORead
stats.DiskWriteBytes = diskIOWrite stats.DiskWriteBytes = diskIOWrite
stats.DiskIoStats[0] = diskReadTime
stats.DiskIoStats[1] = diskWriteTime
stats.DiskIoStats[2] = diskIoUtilPct
stats.DiskIoStats[3] = rAwait
stats.DiskIoStats[4] = wAwait
stats.DiskIoStats[5] = diskWeightedIO
if stats.Root { if stats.Root {
systemStats.DiskReadPs = stats.DiskReadPs systemStats.DiskReadPs = stats.DiskReadPs
systemStats.DiskWritePs = stats.DiskWritePs systemStats.DiskWritePs = stats.DiskWritePs
systemStats.DiskIO[0] = diskIORead systemStats.DiskIO[0] = diskIORead
systemStats.DiskIO[1] = diskIOWrite systemStats.DiskIO[1] = diskIOWrite
systemStats.DiskIoStats[0] = diskReadTime
systemStats.DiskIoStats[1] = diskWriteTime
systemStats.DiskIoStats[2] = diskIoUtilPct
systemStats.DiskIoStats[3] = rAwait
systemStats.DiskIoStats[4] = wAwait
systemStats.DiskIoStats[5] = diskWeightedIO
} }
} }
} }
} }
// getRootMountPoint returns the appropriate root mount point for the system // getRootMountPoint returns the appropriate root mount point for the system.
// On Windows it returns the system drive (e.g. "C:").
// For immutable systems like Fedora Silverblue, it returns /sysroot instead of / // For immutable systems like Fedora Silverblue, it returns /sysroot instead of /
func (a *Agent) getRootMountPoint() string { func (a *Agent) getRootMountPoint() string {
if runtime.GOOS == "windows" {
if sd := os.Getenv("SystemDrive"); sd != "" {
return sd
}
return "C:"
}
// 1. Check if /etc/os-release contains indicators of an immutable system // 1. Check if /etc/os-release contains indicators of an immutable system
if osReleaseContent, err := os.ReadFile("/etc/os-release"); err == nil { if osReleaseContent, err := os.ReadFile("/etc/os-release"); err == nil {
content := string(osReleaseContent) content := string(osReleaseContent)

View File

@@ -530,6 +530,87 @@ func TestAddExtraFilesystemFolders(t *testing.T) {
}) })
} }
func TestAddPartitionExtraFs(t *testing.T) {
makeDiscovery := func(agent *Agent) diskDiscovery {
return diskDiscovery{
agent: agent,
ctx: fsRegistrationContext{
isWindows: false,
efPath: "/extra-filesystems",
diskIoCounters: map[string]disk.IOCountersStat{
"nvme0n1p1": {Name: "nvme0n1p1"},
"nvme1n1": {Name: "nvme1n1"},
},
},
}
}
t.Run("registers direct child of extra-filesystems", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
d := makeDiscovery(agent)
d.addPartitionExtraFs(disk.PartitionStat{
Device: "/dev/nvme0n1p1",
Mountpoint: "/extra-filesystems/nvme0n1p1__caddy1-root",
})
stats, exists := agent.fsStats["nvme0n1p1"]
assert.True(t, exists)
assert.Equal(t, "/extra-filesystems/nvme0n1p1__caddy1-root", stats.Mountpoint)
assert.Equal(t, "caddy1-root", stats.Name)
})
t.Run("skips nested mount under extra-filesystem bind mount", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
d := makeDiscovery(agent)
// These simulate the virtual mounts that appear when host / is bind-mounted
// with disk.Partitions(all=true) — e.g. /proc, /sys, /dev visible under the mount.
for _, nested := range []string{
"/extra-filesystems/nvme0n1p1__caddy1-root/proc",
"/extra-filesystems/nvme0n1p1__caddy1-root/sys",
"/extra-filesystems/nvme0n1p1__caddy1-root/dev",
"/extra-filesystems/nvme0n1p1__caddy1-root/run",
} {
d.addPartitionExtraFs(disk.PartitionStat{Device: "tmpfs", Mountpoint: nested})
}
assert.Empty(t, agent.fsStats)
})
t.Run("registers both direct children, skips their nested mounts", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
d := makeDiscovery(agent)
partitions := []disk.PartitionStat{
{Device: "/dev/nvme0n1p1", Mountpoint: "/extra-filesystems/nvme0n1p1__caddy1-root"},
{Device: "/dev/nvme1n1", Mountpoint: "/extra-filesystems/nvme1n1__caddy1-docker"},
{Device: "proc", Mountpoint: "/extra-filesystems/nvme0n1p1__caddy1-root/proc"},
{Device: "sysfs", Mountpoint: "/extra-filesystems/nvme0n1p1__caddy1-root/sys"},
{Device: "overlay", Mountpoint: "/extra-filesystems/nvme0n1p1__caddy1-root/var/lib/docker"},
}
for _, p := range partitions {
d.addPartitionExtraFs(p)
}
assert.Len(t, agent.fsStats, 2)
assert.Equal(t, "caddy1-root", agent.fsStats["nvme0n1p1"].Name)
assert.Equal(t, "caddy1-docker", agent.fsStats["nvme1n1"].Name)
})
t.Run("skips partition not under extra-filesystems", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
d := makeDiscovery(agent)
d.addPartitionExtraFs(disk.PartitionStat{
Device: "/dev/nvme0n1p1",
Mountpoint: "/",
})
assert.Empty(t, agent.fsStats)
})
}
func TestFindIoDevice(t *testing.T) { func TestFindIoDevice(t *testing.T) {
t.Run("matches by device name", func(t *testing.T) { t.Run("matches by device name", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{ ioCounters := map[string]disk.IOCountersStat{

View File

@@ -25,6 +25,7 @@ import (
"github.com/henrygd/beszel/agent/deltatracker" "github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/agent/utils" "github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/container" "github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/blang/semver" "github.com/blang/semver"
) )
@@ -52,6 +53,7 @@ const (
) )
type dockerManager struct { type dockerManager struct {
agent *Agent // Used to propagate system detail changes back to the agent
client *http.Client // Client to query Docker API client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests sem chan struct{} // Semaphore to limit concurrent container requests
@@ -60,6 +62,7 @@ type dockerManager struct {
containerStatsMap map[string]*container.Stats // Keeps track of container stats containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly) goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
dockerVersionChecked bool // Whether a version probe has completed successfully
isWindows bool // Whether the Docker Engine API is running on Windows isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf decoder *json.Decoder // Reusable JSON decoder that reads from buf
@@ -77,7 +80,7 @@ type dockerManager struct {
// cacheTimeMs -> DeltaTracker for network bytes sent/received // cacheTimeMs -> DeltaTracker for network bytes sent/received
networkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] networkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
networkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] networkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
retrySleep func(time.Duration) lastNetworkReadTime map[uint16]map[string]time.Time // cacheTimeMs -> containerId -> last network read time
} }
// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests // userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests
@@ -86,6 +89,14 @@ type userAgentRoundTripper struct {
userAgent string userAgent string
} }
// dockerVersionResponse contains the /version fields used for engine checks.
type dockerVersionResponse struct {
Version string `json:"Version"`
Components []struct {
Name string `json:"Name"`
} `json:"Components"`
}
// RoundTrip implements the http.RoundTripper interface // RoundTrip implements the http.RoundTripper interface
func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", u.userAgent) req.Header.Set("User-Agent", u.userAgent)
@@ -133,7 +144,14 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats,
return nil, err return nil, err
} }
dm.isWindows = strings.Contains(resp.Header.Get("Server"), "windows") // Detect Podman and Windows from Server header
serverHeader := resp.Header.Get("Server")
if !dm.usingPodman && detectPodmanFromHeader(serverHeader) {
dm.setIsPodman()
}
dm.isWindows = strings.Contains(serverHeader, "windows")
dm.ensureDockerVersionChecked()
containersLength := len(dm.apiContainerList) containersLength := len(dm.apiContainerList)
@@ -285,7 +303,7 @@ func (dm *dockerManager) cycleNetworkDeltasForCacheTime(cacheTimeMs uint16) {
} }
// calculateNetworkStats calculates network sent/receive deltas using DeltaTracker // calculateNetworkStats calculates network sent/receive deltas using DeltaTracker
func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats *container.ApiStats, stats *container.Stats, initialized bool, name string, cacheTimeMs uint16) (uint64, uint64) { func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats *container.ApiStats, name string, cacheTimeMs uint16) (uint64, uint64) {
var total_sent, total_recv uint64 var total_sent, total_recv uint64
for _, v := range apiStats.Networks { for _, v := range apiStats.Networks {
total_sent += v.TxBytes total_sent += v.TxBytes
@@ -304,10 +322,11 @@ func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats
sent_delta_raw := sentTracker.Delta(ctr.IdShort) sent_delta_raw := sentTracker.Delta(ctr.IdShort)
recv_delta_raw := recvTracker.Delta(ctr.IdShort) recv_delta_raw := recvTracker.Delta(ctr.IdShort)
// Calculate bytes per second independently for Tx and Rx if we have previous data // Calculate bytes per second using per-cache-time read time to avoid
// interference between different cache intervals (e.g. 1000ms vs 60000ms)
var sent_delta, recv_delta uint64 var sent_delta, recv_delta uint64
if initialized { if prevReadTime, ok := dm.lastNetworkReadTime[cacheTimeMs][ctr.IdShort]; ok {
millisecondsElapsed := uint64(time.Since(stats.PrevReadTime).Milliseconds()) millisecondsElapsed := uint64(time.Since(prevReadTime).Milliseconds())
if millisecondsElapsed > 0 { if millisecondsElapsed > 0 {
if sent_delta_raw > 0 { if sent_delta_raw > 0 {
sent_delta = sent_delta_raw * 1000 / millisecondsElapsed sent_delta = sent_delta_raw * 1000 / millisecondsElapsed
@@ -542,7 +561,13 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeM
} }
// Calculate network stats using DeltaTracker // Calculate network stats using DeltaTracker
sent_delta, recv_delta := dm.calculateNetworkStats(ctr, res, stats, initialized, name, cacheTimeMs) sent_delta, recv_delta := dm.calculateNetworkStats(ctr, res, name, cacheTimeMs)
// Store per-cache-time network read time for next rate calculation
if dm.lastNetworkReadTime[cacheTimeMs] == nil {
dm.lastNetworkReadTime[cacheTimeMs] = make(map[string]time.Time)
}
dm.lastNetworkReadTime[cacheTimeMs][ctr.IdShort] = time.Now()
// Store current network values for legacy compatibility // Store current network values for legacy compatibility
var total_sent, total_recv uint64 var total_sent, total_recv uint64
@@ -574,10 +599,13 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
for ct := range dm.lastCpuReadTime { for ct := range dm.lastCpuReadTime {
delete(dm.lastCpuReadTime[ct], id) delete(dm.lastCpuReadTime[ct], id)
} }
for ct := range dm.lastNetworkReadTime {
delete(dm.lastNetworkReadTime[ct], id)
}
} }
// Creates a new http client for Docker or Podman API // Creates a new http client for Docker or Podman API
func newDockerManager() *dockerManager { func newDockerManager(agent *Agent) *dockerManager {
dockerHost, exists := utils.GetEnv("DOCKER_HOST") dockerHost, exists := utils.GetEnv("DOCKER_HOST")
if exists { if exists {
// return nil if set to empty string // return nil if set to empty string
@@ -643,6 +671,7 @@ func newDockerManager() *dockerManager {
} }
manager := &dockerManager{ manager := &dockerManager{
agent: agent,
client: &http.Client{ client: &http.Client{
Timeout: timeout, Timeout: timeout,
Transport: userAgentTransport, Transport: userAgentTransport,
@@ -659,51 +688,55 @@ func newDockerManager() *dockerManager {
lastCpuReadTime: make(map[uint16]map[string]time.Time), lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
retrySleep: time.Sleep, lastNetworkReadTime: make(map[uint16]map[string]time.Time),
} }
// If using podman, return client // Best-effort startup probe. If the engine is not ready yet, getDockerStats will
if strings.Contains(dockerHost, "podman") { // retry after the first successful /containers/json request.
manager.usingPodman = true _, _ = manager.checkDockerVersion()
manager.goodDockerVersion = true
return manager
}
// run version check in goroutine to avoid blocking (server may not be ready and requires retries)
go manager.checkDockerVersion()
// give version check a chance to complete before returning
time.Sleep(50 * time.Millisecond)
return manager return manager
} }
// checkDockerVersion checks Docker version and sets goodDockerVersion if at least 25.0.0. // checkDockerVersion checks Docker version and sets goodDockerVersion if at least 25.0.0.
// Versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch. // Versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch.
func (dm *dockerManager) checkDockerVersion() { func (dm *dockerManager) checkDockerVersion() (bool, error) {
var err error resp, err := dm.client.Get("http://localhost/version")
var resp *http.Response if err != nil {
var versionInfo struct { return false, err
Version string `json:"Version"`
} }
const versionMaxTries = 2 if resp.StatusCode != http.StatusOK {
for i := 1; i <= versionMaxTries; i++ { status := resp.Status
resp, err = dm.client.Get("http://localhost/version")
if err == nil && resp.StatusCode == http.StatusOK {
break
}
if resp != nil {
resp.Body.Close() resp.Body.Close()
return false, fmt.Errorf("docker version request failed: %s", status)
} }
if i < versionMaxTries {
slog.Debug("Failed to get Docker version; retrying", "attempt", i, "err", err, "response", resp) var versionInfo dockerVersionResponse
dm.retrySleep(5 * time.Second) serverHeader := resp.Header.Get("Server")
if err := dm.decode(resp, &versionInfo); err != nil {
return false, err
} }
}
if err != nil || resp.StatusCode != http.StatusOK { dm.applyDockerVersionInfo(serverHeader, &versionInfo)
dm.dockerVersionChecked = true
return true, nil
}
// ensureDockerVersionChecked retries the version probe after a successful
// container list request.
func (dm *dockerManager) ensureDockerVersionChecked() {
if dm.dockerVersionChecked {
return return
} }
if err := dm.decode(resp, &versionInfo); err != nil { if _, err := dm.checkDockerVersion(); err != nil {
slog.Debug("Failed to get Docker version", "err", err)
}
}
// applyDockerVersionInfo updates version-dependent behavior from engine metadata.
func (dm *dockerManager) applyDockerVersionInfo(serverHeader string, versionInfo *dockerVersionResponse) {
if detectPodmanEngine(serverHeader, versionInfo) {
dm.setIsPodman()
return return
} }
// if version > 24, one-shot works correctly and we can limit concurrent operations // if version > 24, one-shot works correctly and we can limit concurrent operations
@@ -929,3 +962,46 @@ func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {
func (dm *dockerManager) IsPodman() bool { func (dm *dockerManager) IsPodman() bool {
return dm.usingPodman return dm.usingPodman
} }
// setIsPodman sets the manager to Podman mode and updates system details accordingly.
func (dm *dockerManager) setIsPodman() {
if dm.usingPodman {
return
}
dm.usingPodman = true
dm.goodDockerVersion = true
dm.dockerVersionChecked = true
// keep system details updated - this may be detected late if server isn't ready when
// agent starts, so make sure we notify the hub if this happens later.
if dm.agent != nil {
dm.agent.updateSystemDetails(func(details *system.Details) {
details.Podman = true
})
}
}
// detectPodmanFromHeader identifies Podman from the Docker API server header.
func detectPodmanFromHeader(server string) bool {
return strings.HasPrefix(server, "Libpod")
}
// detectPodmanFromVersion identifies Podman from the version payload.
func detectPodmanFromVersion(versionInfo *dockerVersionResponse) bool {
if versionInfo == nil {
return false
}
for _, component := range versionInfo.Components {
if strings.HasPrefix(component.Name, "Podman") {
return true
}
}
return false
}
// detectPodmanEngine checks both header and version metadata for Podman.
func detectPodmanEngine(serverHeader string, versionInfo *dockerVersionResponse) bool {
if detectPodmanFromHeader(serverHeader) {
return true
}
return detectPodmanFromVersion(versionInfo)
}

View File

@@ -408,6 +408,7 @@ func TestCalculateNetworkStats(t *testing.T) {
dm := &dockerManager{ dm := &dockerManager{
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
} }
cacheTimeMs := uint16(30000) cacheTimeMs := uint16(30000)
@@ -423,6 +424,11 @@ func TestCalculateNetworkStats(t *testing.T) {
dm.networkSentTrackers[cacheTimeMs] = sentTracker dm.networkSentTrackers[cacheTimeMs] = sentTracker
dm.networkRecvTrackers[cacheTimeMs] = recvTracker dm.networkRecvTrackers[cacheTimeMs] = recvTracker
// Set per-cache-time network read time (1 second ago)
dm.lastNetworkReadTime[cacheTimeMs] = map[string]time.Time{
"container1": time.Now().Add(-time.Second),
}
ctr := &container.ApiInfo{ ctr := &container.ApiInfo{
IdShort: "container1", IdShort: "container1",
} }
@@ -433,12 +439,8 @@ func TestCalculateNetworkStats(t *testing.T) {
}, },
} }
stats := &container.Stats{
PrevReadTime: time.Now().Add(-time.Second), // 1 second ago
}
// Test with initialized container // Test with initialized container
sent, recv := dm.calculateNetworkStats(ctr, apiStats, stats, true, "test-container", cacheTimeMs) sent, recv := dm.calculateNetworkStats(ctr, apiStats, "test-container", cacheTimeMs)
// Should return calculated byte rates per second // Should return calculated byte rates per second
assert.GreaterOrEqual(t, sent, uint64(0)) assert.GreaterOrEqual(t, sent, uint64(0))
@@ -446,12 +448,76 @@ func TestCalculateNetworkStats(t *testing.T) {
// Cycle and test one-direction change (Tx only) is reflected independently // Cycle and test one-direction change (Tx only) is reflected independently
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs) dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
dm.lastNetworkReadTime[cacheTimeMs]["container1"] = time.Now().Add(-time.Second)
apiStats.Networks["eth0"] = container.NetworkStats{TxBytes: 2500, RxBytes: 1800} // +500 Tx only apiStats.Networks["eth0"] = container.NetworkStats{TxBytes: 2500, RxBytes: 1800} // +500 Tx only
sent, recv = dm.calculateNetworkStats(ctr, apiStats, stats, true, "test-container", cacheTimeMs) sent, recv = dm.calculateNetworkStats(ctr, apiStats, "test-container", cacheTimeMs)
assert.Greater(t, sent, uint64(0)) assert.Greater(t, sent, uint64(0))
assert.Equal(t, uint64(0), recv) assert.Equal(t, uint64(0), recv)
} }
// TestNetworkStatsCacheTimeIsolation verifies that frequent collections at one cache time
// (e.g. 1000ms) don't cause inflated rates at another cache time (e.g. 60000ms).
// This was a bug where PrevReadTime was shared, so the 60000ms tracker would see a
// large byte delta divided by a tiny elapsed time (set by the 1000ms path).
func TestNetworkStatsCacheTimeIsolation(t *testing.T) {
dm := &dockerManager{
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
}
ctr := &container.ApiInfo{IdShort: "container1"}
fastCache := uint16(1000)
slowCache := uint16(60000)
// Baseline for both cache times at T=0 with 100 bytes total
baseline := &container.ApiStats{
Networks: map[string]container.NetworkStats{
"eth0": {TxBytes: 100, RxBytes: 100},
},
}
dm.calculateNetworkStats(ctr, baseline, "test", fastCache)
dm.calculateNetworkStats(ctr, baseline, "test", slowCache)
// Record read times and cycle both
now := time.Now()
dm.lastNetworkReadTime[fastCache] = map[string]time.Time{"container1": now}
dm.lastNetworkReadTime[slowCache] = map[string]time.Time{"container1": now}
dm.cycleNetworkDeltasForCacheTime(fastCache)
dm.cycleNetworkDeltasForCacheTime(slowCache)
// Simulate many fast (1000ms) collections over ~5 seconds, each adding 10 bytes
totalBytes := uint64(100)
for i := 0; i < 5; i++ {
totalBytes += 10
stats := &container.ApiStats{
Networks: map[string]container.NetworkStats{
"eth0": {TxBytes: totalBytes, RxBytes: totalBytes},
},
}
// Set fast cache read time to 1 second ago
dm.lastNetworkReadTime[fastCache]["container1"] = time.Now().Add(-time.Second)
sent, _ := dm.calculateNetworkStats(ctr, stats, "test", fastCache)
// Fast cache should see ~10 bytes/sec per interval
assert.LessOrEqual(t, sent, uint64(100), "fast cache rate should be reasonable")
dm.cycleNetworkDeltasForCacheTime(fastCache)
}
// Now do slow cache collection — total delta is 50 bytes over ~5 seconds
// Set slow cache read time to 5 seconds ago (the actual elapsed time)
dm.lastNetworkReadTime[slowCache]["container1"] = time.Now().Add(-5 * time.Second)
finalStats := &container.ApiStats{
Networks: map[string]container.NetworkStats{
"eth0": {TxBytes: totalBytes, RxBytes: totalBytes},
},
}
sent, _ := dm.calculateNetworkStats(ctr, finalStats, "test", slowCache)
// Slow cache rate should be ~10 bytes/sec (50 bytes / 5 seconds), NOT 100x inflated
assert.LessOrEqual(t, sent, uint64(100), "slow cache rate should NOT be inflated by fast cache collections")
assert.GreaterOrEqual(t, sent, uint64(1), "slow cache should still report some traffic")
}
func TestDockerManagerCreation(t *testing.T) { func TestDockerManagerCreation(t *testing.T) {
// Test that dockerManager can be created without panicking // Test that dockerManager can be created without panicking
dm := &dockerManager{ dm := &dockerManager{
@@ -460,6 +526,7 @@ func TestDockerManagerCreation(t *testing.T) {
lastCpuReadTime: make(map[uint16]map[string]time.Time), lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
} }
assert.NotNil(t, dm) assert.NotNil(t, dm)
@@ -467,63 +534,58 @@ func TestDockerManagerCreation(t *testing.T) {
assert.NotNil(t, dm.lastCpuSystem) assert.NotNil(t, dm.lastCpuSystem)
assert.NotNil(t, dm.networkSentTrackers) assert.NotNil(t, dm.networkSentTrackers)
assert.NotNil(t, dm.networkRecvTrackers) assert.NotNil(t, dm.networkRecvTrackers)
assert.NotNil(t, dm.lastNetworkReadTime)
} }
func TestCheckDockerVersion(t *testing.T) { func TestCheckDockerVersion(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
responses []struct {
statusCode int statusCode int
body string body string
} server string
expectSuccess bool
expectedGood bool expectedGood bool
expectedRequests int expectedPodman bool
expectError bool
expectedRequest string
}{ }{
{ {
name: "200 with good version on first try", name: "good docker version",
responses: []struct { statusCode: http.StatusOK,
statusCode int body: `{"Version":"25.0.1"}`,
body string expectSuccess: true,
}{
{http.StatusOK, `{"Version":"25.0.1"}`},
},
expectedGood: true, expectedGood: true,
expectedRequests: 1, expectedPodman: false,
expectedRequest: "/version",
}, },
{ {
name: "200 with old version on first try", name: "old docker version",
responses: []struct { statusCode: http.StatusOK,
statusCode int body: `{"Version":"24.0.7"}`,
body string expectSuccess: true,
}{
{http.StatusOK, `{"Version":"24.0.7"}`},
},
expectedGood: false, expectedGood: false,
expectedRequests: 1, expectedPodman: false,
expectedRequest: "/version",
}, },
{ {
name: "non-200 then 200 with good version", name: "podman from server header",
responses: []struct { statusCode: http.StatusOK,
statusCode int body: `{"Version":"5.5.0"}`,
body string server: "Libpod/5.5.0",
}{ expectSuccess: true,
{http.StatusServiceUnavailable, `"not ready"`},
{http.StatusOK, `{"Version":"25.1.0"}`},
},
expectedGood: true, expectedGood: true,
expectedRequests: 2, expectedPodman: true,
expectedRequest: "/version",
}, },
{ {
name: "non-200 on all retries", name: "non-200 response",
responses: []struct { statusCode: http.StatusServiceUnavailable,
statusCode int body: `"not ready"`,
body string expectSuccess: false,
}{
{http.StatusInternalServerError, `"error"`},
{http.StatusUnauthorized, `"error"`},
},
expectedGood: false, expectedGood: false,
expectedRequests: 2, expectedPodman: false,
expectError: true,
expectedRequest: "/version",
}, },
} }
@@ -531,13 +593,13 @@ func TestCheckDockerVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
requestCount := 0 requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
idx := requestCount
requestCount++ requestCount++
if idx >= len(tt.responses) { assert.Equal(t, tt.expectedRequest, r.URL.EscapedPath())
idx = len(tt.responses) - 1 if tt.server != "" {
w.Header().Set("Server", tt.server)
} }
w.WriteHeader(tt.responses[idx].statusCode) w.WriteHeader(tt.statusCode)
fmt.Fprint(w, tt.responses[idx].body) fmt.Fprint(w, tt.body)
})) }))
defer server.Close() defer server.Close()
@@ -549,17 +611,24 @@ func TestCheckDockerVersion(t *testing.T) {
}, },
}, },
}, },
retrySleep: func(time.Duration) {},
} }
dm.checkDockerVersion() success, err := dm.checkDockerVersion()
assert.Equal(t, tt.expectSuccess, success)
assert.Equal(t, tt.expectSuccess, dm.dockerVersionChecked)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion) assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedRequests, requestCount) assert.Equal(t, tt.expectedPodman, dm.usingPodman)
assert.Equal(t, 1, requestCount)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}) })
} }
t.Run("request error on all retries", func(t *testing.T) { t.Run("request error", func(t *testing.T) {
requestCount := 0 requestCount := 0
dm := &dockerManager{ dm := &dockerManager{
client: &http.Client{ client: &http.Client{
@@ -570,16 +639,171 @@ func TestCheckDockerVersion(t *testing.T) {
}, },
}, },
}, },
retrySleep: func(time.Duration) {},
} }
dm.checkDockerVersion() success, err := dm.checkDockerVersion()
assert.False(t, success)
require.Error(t, err)
assert.False(t, dm.dockerVersionChecked)
assert.False(t, dm.goodDockerVersion) assert.False(t, dm.goodDockerVersion)
assert.Equal(t, 2, requestCount) assert.False(t, dm.usingPodman)
assert.Equal(t, 1, requestCount)
}) })
} }
// newDockerManagerForVersionTest creates a dockerManager wired to a test server.
func newDockerManagerForVersionTest(server *httptest.Server) *dockerManager {
return &dockerManager{
client: &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
return net.Dial(network, server.Listener.Addr().String())
},
},
},
containerStatsMap: make(map[string]*container.Stats),
lastCpuContainer: make(map[uint16]map[string]uint64),
lastCpuSystem: make(map[uint16]map[string]uint64),
lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
}
}
func TestGetDockerStatsChecksDockerVersionAfterContainerList(t *testing.T) {
tests := []struct {
name string
containerServer string
versionServer string
versionBody string
expectedGood bool
expectedPodman bool
}{
{
name: "200 with good version on first try",
versionBody: `{"Version":"25.0.1"}`,
expectedGood: true,
expectedPodman: false,
},
{
name: "200 with old version on first try",
versionBody: `{"Version":"24.0.7"}`,
expectedGood: false,
expectedPodman: false,
},
{
name: "podman detected from server header",
containerServer: "Libpod/5.5.0",
expectedGood: true,
expectedPodman: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestCounts := map[string]int{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCounts[r.URL.EscapedPath()]++
switch r.URL.EscapedPath() {
case "/containers/json":
if tt.containerServer != "" {
w.Header().Set("Server", tt.containerServer)
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[]`)
case "/version":
if tt.versionServer != "" {
w.Header().Set("Server", tt.versionServer)
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, tt.versionBody)
default:
t.Fatalf("unexpected path: %s", r.URL.EscapedPath())
}
}))
defer server.Close()
dm := newDockerManagerForVersionTest(server)
stats, err := dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.True(t, dm.dockerVersionChecked)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedPodman, dm.usingPodman)
assert.Equal(t, 1, requestCounts["/containers/json"])
if tt.expectedPodman {
assert.Equal(t, 0, requestCounts["/version"])
} else {
assert.Equal(t, 1, requestCounts["/version"])
}
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedPodman, dm.usingPodman)
assert.Equal(t, 2, requestCounts["/containers/json"])
if tt.expectedPodman {
assert.Equal(t, 0, requestCounts["/version"])
} else {
assert.Equal(t, 1, requestCounts["/version"])
}
})
}
}
func TestGetDockerStatsRetriesVersionCheckUntilSuccess(t *testing.T) {
requestCounts := map[string]int{}
versionStatuses := []int{http.StatusServiceUnavailable, http.StatusOK}
versionBodies := []string{`"not ready"`, `{"Version":"25.1.0"}`}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCounts[r.URL.EscapedPath()]++
switch r.URL.EscapedPath() {
case "/containers/json":
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[]`)
case "/version":
idx := requestCounts["/version"] - 1
if idx >= len(versionStatuses) {
idx = len(versionStatuses) - 1
}
w.WriteHeader(versionStatuses[idx])
fmt.Fprint(w, versionBodies[idx])
default:
t.Fatalf("unexpected path: %s", r.URL.EscapedPath())
}
}))
defer server.Close()
dm := newDockerManagerForVersionTest(server)
stats, err := dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.False(t, dm.dockerVersionChecked)
assert.False(t, dm.goodDockerVersion)
assert.Equal(t, 1, requestCounts["/version"])
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.True(t, dm.dockerVersionChecked)
assert.True(t, dm.goodDockerVersion)
assert.Equal(t, 2, requestCounts["/containers/json"])
assert.Equal(t, 2, requestCounts["/version"])
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.Equal(t, 3, requestCounts["/containers/json"])
assert.Equal(t, 2, requestCounts["/version"])
}
func TestCycleCpuDeltas(t *testing.T) { func TestCycleCpuDeltas(t *testing.T) {
dm := &dockerManager{ dm := &dockerManager{
lastCpuContainer: map[uint16]map[string]uint64{ lastCpuContainer: map[uint16]map[string]uint64{
@@ -651,6 +875,7 @@ func TestDockerStatsWithMockData(t *testing.T) {
lastCpuReadTime: make(map[uint16]map[string]time.Time), lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
containerStatsMap: make(map[string]*container.Stats), containerStatsMap: make(map[string]*container.Stats),
} }
@@ -796,23 +1021,22 @@ func TestNetworkStatsCalculationWithRealData(t *testing.T) {
dm := &dockerManager{ dm := &dockerManager{
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
} }
ctr := &container.ApiInfo{IdShort: "test-container"} ctr := &container.ApiInfo{IdShort: "test-container"}
cacheTimeMs := uint16(30000) // Test with 30 second cache cacheTimeMs := uint16(30000) // Test with 30 second cache
// Use exact timing for deterministic results // First call sets baseline (no previous read time, so rates should be 0)
exactly1000msAgo := time.Now().Add(-1000 * time.Millisecond) sent1, recv1 := dm.calculateNetworkStats(ctr, apiStats1, "test", cacheTimeMs)
stats := &container.Stats{
PrevReadTime: exactly1000msAgo,
}
// First call sets baseline
sent1, recv1 := dm.calculateNetworkStats(ctr, apiStats1, stats, true, "test", cacheTimeMs)
assert.Equal(t, uint64(0), sent1) assert.Equal(t, uint64(0), sent1)
assert.Equal(t, uint64(0), recv1) assert.Equal(t, uint64(0), recv1)
// Cycle to establish baseline for this cache time // Record read time and cycle to establish baseline for this cache time
exactly1000msAgo := time.Now().Add(-1000 * time.Millisecond)
dm.lastNetworkReadTime[cacheTimeMs] = map[string]time.Time{
"test-container": exactly1000msAgo,
}
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs) dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
// Calculate expected results precisely // Calculate expected results precisely
@@ -823,7 +1047,7 @@ func TestNetworkStatsCalculationWithRealData(t *testing.T) {
expectedRecvRate := deltaRecv * 1000 / expectedElapsedMs // Should be exactly 1000000 expectedRecvRate := deltaRecv * 1000 / expectedElapsedMs // Should be exactly 1000000
// Second call with changed data // Second call with changed data
sent2, recv2 := dm.calculateNetworkStats(ctr, apiStats2, stats, true, "test", cacheTimeMs) sent2, recv2 := dm.calculateNetworkStats(ctr, apiStats2, "test", cacheTimeMs)
// Should be exactly the expected rates (no tolerance needed) // Should be exactly the expected rates (no tolerance needed)
assert.Equal(t, expectedSentRate, sent2) assert.Equal(t, expectedSentRate, sent2)
@@ -831,12 +1055,13 @@ func TestNetworkStatsCalculationWithRealData(t *testing.T) {
// Bad speed cap: set absurd delta over 1ms and expect 0 due to cap // Bad speed cap: set absurd delta over 1ms and expect 0 due to cap
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs) dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
stats.PrevReadTime = time.Now().Add(-1 * time.Millisecond) dm.lastNetworkReadTime[cacheTimeMs]["test-container"] = time.Now().Add(-1 * time.Millisecond)
apiStats1.Networks["eth0"] = container.NetworkStats{TxBytes: 0, RxBytes: 0} apiStats1.Networks["eth0"] = container.NetworkStats{TxBytes: 0, RxBytes: 0}
apiStats2.Networks["eth0"] = container.NetworkStats{TxBytes: 10 * 1024 * 1024 * 1024, RxBytes: 0} // 10GB delta apiStats2.Networks["eth0"] = container.NetworkStats{TxBytes: 10 * 1024 * 1024 * 1024, RxBytes: 0} // 10GB delta
_, _ = dm.calculateNetworkStats(ctr, apiStats1, stats, true, "test", cacheTimeMs) // baseline _, _ = dm.calculateNetworkStats(ctr, apiStats1, "test", cacheTimeMs) // baseline
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs) dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
sent3, recv3 := dm.calculateNetworkStats(ctr, apiStats2, stats, true, "test", cacheTimeMs) dm.lastNetworkReadTime[cacheTimeMs]["test-container"] = time.Now().Add(-1 * time.Millisecond)
sent3, recv3 := dm.calculateNetworkStats(ctr, apiStats2, "test", cacheTimeMs)
assert.Equal(t, uint64(0), sent3) assert.Equal(t, uint64(0), sent3)
assert.Equal(t, uint64(0), recv3) assert.Equal(t, uint64(0), recv3)
} }
@@ -857,6 +1082,7 @@ func TestContainerStatsEndToEndWithRealData(t *testing.T) {
lastCpuReadTime: make(map[uint16]map[string]time.Time), lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
containerStatsMap: make(map[string]*container.Stats), containerStatsMap: make(map[string]*container.Stats),
} }
@@ -978,6 +1204,7 @@ func TestDockerStatsWorkflow(t *testing.T) {
lastCpuSystem: make(map[uint16]map[string]uint64), lastCpuSystem: make(map[uint16]map[string]uint64),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
containerStatsMap: make(map[string]*container.Stats), containerStatsMap: make(map[string]*container.Stats),
} }
@@ -1242,6 +1469,7 @@ func TestUpdateContainerStatsUsesPodmanInspectHealthFallback(t *testing.T) {
lastCpuReadTime: make(map[uint16]map[string]time.Time), lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]), networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
} }
ctr := &container.ApiInfo{ ctr := &container.ApiInfo{

View File

@@ -461,7 +461,7 @@ func (gm *GPUManager) discoverGpuCapabilities() gpuCapabilities {
caps.hasNvtop = true caps.hasNvtop = true
} }
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
if _, err := exec.LookPath(macmonCmd); err == nil { if _, err := utils.LookPathHomebrew(macmonCmd); err == nil {
caps.hasMacmon = true caps.hasMacmon = true
} }
if _, err := exec.LookPath(powermetricsCmd); err == nil { if _, err := exec.LookPath(powermetricsCmd); err == nil {
@@ -542,7 +542,7 @@ func (gm *GPUManager) collectorDefinitions(caps gpuCapabilities) map[collectorSo
return map[collectorSource]collectorDefinition{ return map[collectorSource]collectorDefinition{
collectorSourceNVML: { collectorSourceNVML: {
group: collectorGroupNvidia, group: collectorGroupNvidia,
available: caps.hasNvidiaSmi, available: true,
start: func(_ func()) bool { start: func(_ func()) bool {
return gm.startNvmlCollector() return gm.startNvmlCollector()
}, },
@@ -734,9 +734,6 @@ func NewGPUManager() (*GPUManager, error) {
} }
var gm GPUManager var gm GPUManager
caps := gm.discoverGpuCapabilities() caps := gm.discoverGpuCapabilities()
if !hasAnyGpuCollector(caps) {
return nil, fmt.Errorf(noGPUFoundMsg)
}
gm.GpuDataMap = make(map[string]*system.GPUData) gm.GpuDataMap = make(map[string]*system.GPUData)
// Jetson devices should always use tegrastats (ignore GPU_COLLECTOR). // Jetson devices should always use tegrastats (ignore GPU_COLLECTOR).
@@ -745,7 +742,7 @@ func NewGPUManager() (*GPUManager, error) {
return &gm, nil return &gm, nil
} }
// if GPU_COLLECTOR is set, start user-defined collectors. // Respect explicit collector selection before capability auto-detection.
if collectorConfig, ok := utils.GetEnv("GPU_COLLECTOR"); ok && strings.TrimSpace(collectorConfig) != "" { if collectorConfig, ok := utils.GetEnv("GPU_COLLECTOR"); ok && strings.TrimSpace(collectorConfig) != "" {
priorities := parseCollectorPriority(collectorConfig) priorities := parseCollectorPriority(collectorConfig)
if gm.startCollectorsByPriority(priorities, caps) == 0 { if gm.startCollectorsByPriority(priorities, caps) == 0 {
@@ -754,6 +751,10 @@ func NewGPUManager() (*GPUManager, error) {
return &gm, nil return &gm, nil
} }
if !hasAnyGpuCollector(caps) {
return nil, fmt.Errorf(noGPUFoundMsg)
}
// auto-detect and start collectors when GPU_COLLECTOR is unset. // auto-detect and start collectors when GPU_COLLECTOR is unset.
if gm.startCollectorsByPriority(gm.resolveLegacyCollectorPriority(caps), caps) == 0 { if gm.startCollectorsByPriority(gm.resolveLegacyCollectorPriority(caps), caps) == 0 {
return nil, fmt.Errorf(noGPUFoundMsg) return nil, fmt.Errorf(noGPUFoundMsg)

View File

@@ -13,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
) )
@@ -171,7 +172,11 @@ type macmonSample struct {
} }
func (gm *GPUManager) collectMacmonPipe() (err error) { func (gm *GPUManager) collectMacmonPipe() (err error) {
cmd := exec.Command(macmonCmd, "pipe", "-i", strconv.Itoa(macmonIntervalMs)) macmonPath, err := utils.LookPathHomebrew(macmonCmd)
if err != nil {
return err
}
cmd := exec.Command(macmonPath, "pipe", "-i", strconv.Itoa(macmonIntervalMs))
// Avoid blocking if macmon writes to stderr. // Avoid blocking if macmon writes to stderr.
cmd.Stderr = io.Discard cmd.Stderr = io.Discard
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()

View File

@@ -1461,6 +1461,25 @@ func TestNewGPUManagerConfiguredCollectorsMustStart(t *testing.T) {
}) })
} }
func TestCollectorDefinitionsNvmlDoesNotRequireNvidiaSmi(t *testing.T) {
gm := &GPUManager{}
definitions := gm.collectorDefinitions(gpuCapabilities{})
require.Contains(t, definitions, collectorSourceNVML)
assert.True(t, definitions[collectorSourceNVML].available)
}
func TestNewGPUManagerConfiguredNvmlBypassesCapabilityGate(t *testing.T) {
dir := t.TempDir()
t.Setenv("PATH", dir)
t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvml")
gm, err := NewGPUManager()
require.Nil(t, gm)
require.Error(t, err)
assert.Contains(t, err.Error(), "no configured GPU collectors are available")
assert.NotContains(t, err.Error(), noGPUFoundMsg)
}
func TestNewGPUManagerJetsonIgnoresCollectorConfig(t *testing.T) { func TestNewGPUManagerJetsonIgnoresCollectorConfig(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
t.Setenv("PATH", dir) t.Setenv("PATH", dir)

View File

@@ -8,6 +8,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.5" /> <PackageReference Include="LibreHardwareMonitorLib" Version="0.9.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,12 +2,14 @@ package agent
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"path" "path"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"unicode/utf8" "unicode/utf8"
"github.com/henrygd/beszel/agent/utils" "github.com/henrygd/beszel/agent/utils"
@@ -17,13 +19,20 @@ import (
"github.com/shirou/gopsutil/v4/sensors" "github.com/shirou/gopsutil/v4/sensors"
) )
var errTemperatureFetchTimeout = errors.New("temperature collection timed out")
// Matches sensors.TemperaturesWithContext to allow for panic recovery (gopsutil/issues/1832)
type getTempsFn func(ctx context.Context) ([]sensors.TemperatureStat, error)
type SensorConfig struct { type SensorConfig struct {
context context.Context context context.Context
sensors map[string]struct{} sensors map[string]struct{}
primarySensor string primarySensor string
timeout time.Duration
isBlacklist bool isBlacklist bool
hasWildcards bool hasWildcards bool
skipCollection bool skipCollection bool
firstRun bool
} }
func (a *Agent) newSensorConfig() *SensorConfig { func (a *Agent) newSensorConfig() *SensorConfig {
@@ -31,20 +40,29 @@ func (a *Agent) newSensorConfig() *SensorConfig {
sysSensors, _ := utils.GetEnv("SYS_SENSORS") sysSensors, _ := utils.GetEnv("SYS_SENSORS")
sensorsEnvVal, sensorsSet := utils.GetEnv("SENSORS") sensorsEnvVal, sensorsSet := utils.GetEnv("SENSORS")
skipCollection := sensorsSet && sensorsEnvVal == "" skipCollection := sensorsSet && sensorsEnvVal == ""
sensorsTimeout, _ := utils.GetEnv("SENSORS_TIMEOUT")
return a.newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, skipCollection) return a.newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, sensorsTimeout, skipCollection)
} }
// Matches sensors.TemperaturesWithContext to allow for panic recovery (gopsutil/issues/1832)
type getTempsFn func(ctx context.Context) ([]sensors.TemperatureStat, error)
// newSensorConfigWithEnv creates a SensorConfig with the provided environment variables // newSensorConfigWithEnv creates a SensorConfig with the provided environment variables
// sensorsSet indicates if the SENSORS environment variable was explicitly set (even to empty string) // sensorsSet indicates if the SENSORS environment variable was explicitly set (even to empty string)
func (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal string, skipCollection bool) *SensorConfig { func (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, sensorsTimeout string, skipCollection bool) *SensorConfig {
timeout := 2 * time.Second
if sensorsTimeout != "" {
if d, err := time.ParseDuration(sensorsTimeout); err == nil {
timeout = d
} else {
slog.Warn("Invalid SENSORS_TIMEOUT", "value", sensorsTimeout)
}
}
config := &SensorConfig{ config := &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: primarySensor, primarySensor: primarySensor,
timeout: timeout,
skipCollection: skipCollection, skipCollection: skipCollection,
firstRun: true,
sensors: make(map[string]struct{}), sensors: make(map[string]struct{}),
} }
@@ -86,10 +104,12 @@ func (a *Agent) updateTemperatures(systemStats *system.Stats) {
// reset high temp // reset high temp
a.systemInfo.DashboardTemp = 0 a.systemInfo.DashboardTemp = 0
temps, err := a.getTempsWithPanicRecovery(getSensorTemps) temps, err := a.getTempsWithTimeout(getSensorTemps)
if err != nil { if err != nil {
// retry once on panic (gopsutil/issues/1832) // retry once on panic (gopsutil/issues/1832)
temps, err = a.getTempsWithPanicRecovery(getSensorTemps) if !errors.Is(err, errTemperatureFetchTimeout) {
temps, err = a.getTempsWithTimeout(getSensorTemps)
}
if err != nil { if err != nil {
slog.Warn("Error updating temperatures", "err", err) slog.Warn("Error updating temperatures", "err", err)
if len(systemStats.Temperatures) > 0 { if len(systemStats.Temperatures) > 0 {
@@ -152,6 +172,34 @@ func (a *Agent) getTempsWithPanicRecovery(getTemps getTempsFn) (temps []sensors.
return return
} }
func (a *Agent) getTempsWithTimeout(getTemps getTempsFn) ([]sensors.TemperatureStat, error) {
type result struct {
temps []sensors.TemperatureStat
err error
}
// Use a longer timeout on the first run to allow for initialization
// (e.g. Windows LHM subprocess startup)
timeout := a.sensorConfig.timeout
if a.sensorConfig.firstRun {
a.sensorConfig.firstRun = false
timeout = 10 * time.Second
}
resultCh := make(chan result, 1)
go func() {
temps, err := a.getTempsWithPanicRecovery(getTemps)
resultCh <- result{temps: temps, err: err}
}()
select {
case res := <-resultCh:
return res.temps, res.err
case <-time.After(timeout):
return nil, errTemperatureFetchTimeout
}
}
// isValidSensor checks if a sensor is valid based on the sensor name and the sensor config // isValidSensor checks if a sensor is valid based on the sensor name and the sensor config
func isValidSensor(sensorName string, config *SensorConfig) bool { func isValidSensor(sensorName string, config *SensorConfig) bool {
// if no sensors configured, everything is valid // if no sensors configured, everything is valid

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
@@ -167,6 +168,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
primarySensor string primarySensor string
sysSensors string sysSensors string
sensors string sensors string
sensorsTimeout string
skipCollection bool skipCollection bool
expectedConfig *SensorConfig expectedConfig *SensorConfig
}{ }{
@@ -178,12 +180,37 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "", primarySensor: "",
timeout: 2 * time.Second,
sensors: map[string]struct{}{}, sensors: map[string]struct{}{},
isBlacklist: false, isBlacklist: false,
hasWildcards: false, hasWildcards: false,
skipCollection: false, skipCollection: false,
}, },
}, },
{
name: "Custom timeout",
primarySensor: "",
sysSensors: "",
sensors: "",
sensorsTimeout: "5s",
expectedConfig: &SensorConfig{
context: context.Background(),
timeout: 5 * time.Second,
sensors: map[string]struct{}{},
},
},
{
name: "Invalid timeout falls back to default",
primarySensor: "",
sysSensors: "",
sensors: "",
sensorsTimeout: "notaduration",
expectedConfig: &SensorConfig{
context: context.Background(),
timeout: 2 * time.Second,
sensors: map[string]struct{}{},
},
},
{ {
name: "Explicitly set to empty string", name: "Explicitly set to empty string",
primarySensor: "", primarySensor: "",
@@ -193,6 +220,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "", primarySensor: "",
timeout: 2 * time.Second,
sensors: map[string]struct{}{}, sensors: map[string]struct{}{},
isBlacklist: false, isBlacklist: false,
hasWildcards: false, hasWildcards: false,
@@ -207,6 +235,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "cpu_temp", primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{}, sensors: map[string]struct{}{},
isBlacklist: false, isBlacklist: false,
hasWildcards: false, hasWildcards: false,
@@ -220,6 +249,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "cpu_temp", primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{ sensors: map[string]struct{}{
"cpu_temp": {}, "cpu_temp": {},
"gpu_temp": {}, "gpu_temp": {},
@@ -236,6 +266,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "cpu_temp", primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{ sensors: map[string]struct{}{
"cpu_temp": {}, "cpu_temp": {},
"gpu_temp": {}, "gpu_temp": {},
@@ -252,6 +283,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "cpu_temp", primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{ sensors: map[string]struct{}{
"cpu_*": {}, "cpu_*": {},
"gpu_temp": {}, "gpu_temp": {},
@@ -268,6 +300,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
context: context.Background(), context: context.Background(),
primarySensor: "cpu_temp", primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{ sensors: map[string]struct{}{
"cpu_*": {}, "cpu_*": {},
"gpu_temp": {}, "gpu_temp": {},
@@ -283,6 +316,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
sensors: "cpu_temp", sensors: "cpu_temp",
expectedConfig: &SensorConfig{ expectedConfig: &SensorConfig{
primarySensor: "cpu_temp", primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{ sensors: map[string]struct{}{
"cpu_temp": {}, "cpu_temp": {},
}, },
@@ -294,7 +328,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.skipCollection) result := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.sensorsTimeout, tt.skipCollection)
// Check primary sensor // Check primary sensor
assert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor) assert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor)
@@ -313,6 +347,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
// Check flags // Check flags
assert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist) assert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist)
assert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards) assert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards)
assert.Equal(t, tt.expectedConfig.timeout, result.timeout)
// Check context // Check context
if tt.sysSensors != "" { if tt.sysSensors != "" {
@@ -332,12 +367,14 @@ func TestNewSensorConfig(t *testing.T) {
t.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary") t.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary")
t.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path") t.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path")
t.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3") t.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3")
t.Setenv("BESZEL_AGENT_SENSORS_TIMEOUT", "7s")
agent := &Agent{} agent := &Agent{}
result := agent.newSensorConfig() result := agent.newSensorConfig()
// Verify results // Verify results
assert.Equal(t, "test_primary", result.primarySensor) assert.Equal(t, "test_primary", result.primarySensor)
assert.Equal(t, 7*time.Second, result.timeout)
assert.NotNil(t, result.sensors) assert.NotNil(t, result.sensors)
assert.Equal(t, 3, len(result.sensors)) assert.Equal(t, 3, len(result.sensors))
assert.True(t, result.hasWildcards) assert.True(t, result.hasWildcards)
@@ -526,3 +563,59 @@ func TestGetTempsWithPanicRecovery(t *testing.T) {
}) })
} }
} }
func TestGetTempsWithTimeout(t *testing.T) {
agent := &Agent{
sensorConfig: &SensorConfig{
context: context.Background(),
timeout: 10 * time.Millisecond,
},
}
t.Run("returns temperatures before timeout", func(t *testing.T) {
temps, err := agent.getTempsWithTimeout(func(ctx context.Context) ([]sensors.TemperatureStat, error) {
return []sensors.TemperatureStat{{SensorKey: "cpu_temp", Temperature: 42}}, nil
})
require.NoError(t, err)
require.Len(t, temps, 1)
assert.Equal(t, "cpu_temp", temps[0].SensorKey)
})
t.Run("returns timeout error when collector hangs", func(t *testing.T) {
temps, err := agent.getTempsWithTimeout(func(ctx context.Context) ([]sensors.TemperatureStat, error) {
time.Sleep(50 * time.Millisecond)
return []sensors.TemperatureStat{{SensorKey: "cpu_temp", Temperature: 42}}, nil
})
assert.Nil(t, temps)
assert.ErrorIs(t, err, errTemperatureFetchTimeout)
})
}
func TestUpdateTemperaturesSkipsOnTimeout(t *testing.T) {
agent := &Agent{
systemInfo: system.Info{DashboardTemp: 99},
sensorConfig: &SensorConfig{
context: context.Background(),
timeout: 10 * time.Millisecond,
},
}
t.Cleanup(func() {
getSensorTemps = sensors.TemperaturesWithContext
})
getSensorTemps = func(ctx context.Context) ([]sensors.TemperatureStat, error) {
time.Sleep(50 * time.Millisecond)
return nil, nil
}
stats := &system.Stats{
Temperatures: map[string]float64{"stale": 50},
}
agent.updateTemperatures(stats)
assert.Equal(t, 0.0, agent.systemInfo.DashboardTemp)
assert.Equal(t, map[string]float64{}, stats.Temperatures)
}

View File

@@ -193,7 +193,7 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes
// handleLegacyStats serves the legacy one-shot stats payload for older hubs // handleLegacyStats serves the legacy one-shot stats payload for older hubs
func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error { func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {
stats := a.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000}) stats := a.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs})
return a.writeToSession(w, stats, hubVersion) return a.writeToSession(w, stats, hubVersion)
} }

View File

@@ -31,6 +31,9 @@ type SmartManager struct {
lastScanTime time.Time lastScanTime time.Time
smartctlPath string smartctlPath string
excludedDevices map[string]struct{} excludedDevices map[string]struct{}
darwinNvmeOnce sync.Once
darwinNvmeCapacity map[string]uint64 // serial → bytes cache, written once via darwinNvmeOnce
darwinNvmeProvider func() ([]byte, error) // overridable for testing
} }
type scanOutput struct { type scanOutput struct {
@@ -1033,6 +1036,52 @@ func parseScsiGigabytesProcessed(value string) int64 {
return parsed return parsed
} }
// lookupDarwinNvmeCapacity returns the capacity in bytes for a given NVMe serial number on Darwin.
// It uses system_profiler SPNVMeDataType to get capacity since Apple SSDs don't report user_capacity
// via smartctl. Results are cached after the first call via sync.Once.
func (sm *SmartManager) lookupDarwinNvmeCapacity(serial string) uint64 {
sm.darwinNvmeOnce.Do(func() {
sm.darwinNvmeCapacity = make(map[string]uint64)
provider := sm.darwinNvmeProvider
if provider == nil {
provider = func() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return exec.CommandContext(ctx, "system_profiler", "SPNVMeDataType", "-json").Output()
}
}
out, err := provider()
if err != nil {
slog.Debug("system_profiler NVMe lookup failed", "err", err)
return
}
var result struct {
SPNVMeDataType []struct {
Items []struct {
DeviceSerial string `json:"device_serial"`
SizeInBytes uint64 `json:"size_in_bytes"`
} `json:"_items"`
} `json:"SPNVMeDataType"`
}
if err := json.Unmarshal(out, &result); err != nil {
slog.Debug("system_profiler NVMe parse failed", "err", err)
return
}
for _, controller := range result.SPNVMeDataType {
for _, item := range controller.Items {
if item.DeviceSerial != "" && item.SizeInBytes > 0 {
sm.darwinNvmeCapacity[item.DeviceSerial] = item.SizeInBytes
}
}
}
})
return sm.darwinNvmeCapacity[serial]
}
// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap // parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap
// Returns hasValidData and exitStatus // Returns hasValidData and exitStatus
func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) { func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
@@ -1069,6 +1118,9 @@ func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
smartData.SerialNumber = data.SerialNumber smartData.SerialNumber = data.SerialNumber
smartData.FirmwareVersion = data.FirmwareVersion smartData.FirmwareVersion = data.FirmwareVersion
smartData.Capacity = data.UserCapacity.Bytes smartData.Capacity = data.UserCapacity.Bytes
if smartData.Capacity == 0 && (runtime.GOOS == "darwin" || sm.darwinNvmeProvider != nil) {
smartData.Capacity = sm.lookupDarwinNvmeCapacity(data.SerialNumber)
}
smartData.Temperature = data.NVMeSmartHealthInformationLog.Temperature smartData.Temperature = data.NVMeSmartHealthInformationLog.Temperature
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed) smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
smartData.DiskName = data.Device.Name smartData.DiskName = data.Device.Name
@@ -1104,32 +1156,21 @@ func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
// detectSmartctl checks if smartctl is installed, returns an error if not // detectSmartctl checks if smartctl is installed, returns an error if not
func (sm *SmartManager) detectSmartctl() (string, error) { func (sm *SmartManager) detectSmartctl() (string, error) {
isWindows := runtime.GOOS == "windows" if runtime.GOOS == "windows" {
// Load embedded smartctl.exe for Windows amd64 builds. // Load embedded smartctl.exe for Windows amd64 builds.
if isWindows && runtime.GOARCH == "amd64" { if runtime.GOARCH == "amd64" {
if path, err := ensureEmbeddedSmartctl(); err == nil { if path, err := ensureEmbeddedSmartctl(); err == nil {
return path, nil return path, nil
} }
} }
// Try to find smartctl in the default installation location
if path, err := exec.LookPath("smartctl"); err == nil { const location = "C:\\Program Files\\smartmontools\\bin\\smartctl.exe"
return path, nil
}
locations := []string{}
if isWindows {
locations = append(locations,
"C:\\Program Files\\smartmontools\\bin\\smartctl.exe",
)
} else {
locations = append(locations, "/opt/homebrew/bin/smartctl")
}
for _, location := range locations {
if _, err := os.Stat(location); err == nil { if _, err := os.Stat(location); err == nil {
return location, nil return location, nil
} }
} }
return "", errors.New("smartctl not found")
return utils.LookPathHomebrew("smartctl")
} }
// isNvmeControllerPath checks if the path matches an NVMe controller pattern // isNvmeControllerPath checks if the path matches an NVMe controller pattern

View File

@@ -1199,3 +1199,81 @@ func TestIsNvmeControllerPath(t *testing.T) {
}) })
} }
} }
func TestParseSmartForNvmeAppleSSD(t *testing.T) {
// Apple SSDs don't report user_capacity via smartctl; capacity should be fetched
// from system_profiler via the darwinNvmeProvider fallback.
fixturePath := filepath.Join("test-data", "smart", "apple_nvme.json")
data, err := os.ReadFile(fixturePath)
require.NoError(t, err)
providerCalls := 0
fakeProvider := func() ([]byte, error) {
providerCalls++
return []byte(`{
"SPNVMeDataType": [{
"_items": [{
"device_serial": "0ba0147940253c15",
"size_in_bytes": 251000193024
}]
}]
}`), nil
}
sm := &SmartManager{
SmartDataMap: make(map[string]*smart.SmartData),
darwinNvmeProvider: fakeProvider,
}
hasData, _ := sm.parseSmartForNvme(data)
require.True(t, hasData)
deviceData, ok := sm.SmartDataMap["0ba0147940253c15"]
require.True(t, ok)
assert.Equal(t, "APPLE SSD AP0256Q", deviceData.ModelName)
assert.Equal(t, uint64(251000193024), deviceData.Capacity)
assert.Equal(t, uint8(42), deviceData.Temperature)
assert.Equal(t, "PASSED", deviceData.SmartStatus)
assert.Equal(t, 1, providerCalls, "system_profiler should be called once")
// Second parse: provider should NOT be called again (cache hit)
_, _ = sm.parseSmartForNvme(data)
assert.Equal(t, 1, providerCalls, "system_profiler should not be called again after caching")
}
func TestLookupDarwinNvmeCapacityMultipleDisks(t *testing.T) {
fakeProvider := func() ([]byte, error) {
return []byte(`{
"SPNVMeDataType": [
{
"_items": [
{"device_serial": "serial-disk0", "size_in_bytes": 251000193024},
{"device_serial": "serial-disk1", "size_in_bytes": 1000204886016}
]
},
{
"_items": [
{"device_serial": "serial-disk2", "size_in_bytes": 512110190592}
]
}
]
}`), nil
}
sm := &SmartManager{darwinNvmeProvider: fakeProvider}
assert.Equal(t, uint64(251000193024), sm.lookupDarwinNvmeCapacity("serial-disk0"))
assert.Equal(t, uint64(1000204886016), sm.lookupDarwinNvmeCapacity("serial-disk1"))
assert.Equal(t, uint64(512110190592), sm.lookupDarwinNvmeCapacity("serial-disk2"))
assert.Equal(t, uint64(0), sm.lookupDarwinNvmeCapacity("unknown-serial"))
}
func TestLookupDarwinNvmeCapacityProviderError(t *testing.T) {
fakeProvider := func() ([]byte, error) {
return nil, errors.New("system_profiler not found")
}
sm := &SmartManager{darwinNvmeProvider: fakeProvider}
assert.Equal(t, uint64(0), sm.lookupDarwinNvmeCapacity("any-serial"))
// Cache should be initialized even on error so we don't retry (Once already fired)
assert.NotNil(t, sm.darwinNvmeCapacity)
}

View File

@@ -8,7 +8,6 @@ import (
"os" "os"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/henrygd/beszel" "github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery" "github.com/henrygd/beszel/agent/battery"
@@ -23,13 +22,6 @@ import (
"github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/mem"
) )
// prevDisk stores previous per-device disk counters for a given cache interval
type prevDisk struct {
readBytes uint64
writeBytes uint64
at time.Time
}
// Sets initial / non-changing values about the host system // Sets initial / non-changing values about the host system
func (a *Agent) refreshSystemDetails() { func (a *Agent) refreshSystemDetails() {
a.systemInfo.AgentVersion = beszel.Version a.systemInfo.AgentVersion = beszel.Version
@@ -115,6 +107,26 @@ func (a *Agent) refreshSystemDetails() {
} }
} }
// attachSystemDetails returns details only for fresh default-interval responses.
func (a *Agent) attachSystemDetails(data *system.CombinedData, cacheTimeMs uint16, includeRequested bool) *system.CombinedData {
if cacheTimeMs != defaultDataCacheTimeMs || (!includeRequested && !a.detailsDirty) {
return data
}
// copy data to avoid adding details to the original cached struct
response := *data
response.Details = &a.systemDetails
a.detailsDirty = false
return &response
}
// updateSystemDetails applies a mutation to the static details payload and marks
// it for inclusion on the next fresh default-interval response.
func (a *Agent) updateSystemDetails(updateFunc func(details *system.Details)) {
updateFunc(&a.systemDetails)
a.detailsDirty = true
}
// Returns current info, stats about the host system // Returns current info, stats about the host system
func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats { func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
var systemStats system.Stats var systemStats system.Stats

61
agent/system_test.go Normal file
View File

@@ -0,0 +1,61 @@
package agent
import (
"testing"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGatherStatsDoesNotAttachDetailsToCachedRequests(t *testing.T) {
agent := &Agent{
cache: NewSystemDataCache(),
systemDetails: system.Details{Hostname: "updated-host", Podman: true},
detailsDirty: true,
}
cached := &system.CombinedData{
Info: system.Info{Hostname: "cached-host"},
}
agent.cache.Set(cached, defaultDataCacheTimeMs)
response := agent.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs})
assert.Same(t, cached, response)
assert.Nil(t, response.Details)
assert.True(t, agent.detailsDirty)
assert.Equal(t, "cached-host", response.Info.Hostname)
assert.Nil(t, cached.Details)
secondResponse := agent.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs})
assert.Same(t, cached, secondResponse)
assert.Nil(t, secondResponse.Details)
}
func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) {
agent := &Agent{}
agent.updateSystemDetails(func(details *system.Details) {
details.Hostname = "updated-host"
details.Podman = true
})
assert.True(t, agent.detailsDirty)
assert.Equal(t, "updated-host", agent.systemDetails.Hostname)
assert.True(t, agent.systemDetails.Podman)
original := &system.CombinedData{}
realTimeResponse := agent.attachSystemDetails(original, 1000, true)
assert.Same(t, original, realTimeResponse)
assert.Nil(t, realTimeResponse.Details)
assert.True(t, agent.detailsDirty)
response := agent.attachSystemDetails(original, defaultDataCacheTimeMs, false)
require.NotNil(t, response.Details)
assert.NotSame(t, original, response)
assert.Equal(t, "updated-host", response.Details.Hostname)
assert.True(t, response.Details.Podman)
assert.False(t, agent.detailsDirty)
assert.Nil(t, original.Details)
}

View File

@@ -0,0 +1,51 @@
{
"json_format_version": [1, 0],
"smartctl": {
"version": [7, 4],
"argv": ["smartctl", "-aix", "-j", "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddy(ANS2)/RTBuddyService/AppleANS3NVMeController/NS_01@1"],
"exit_status": 4
},
"device": {
"name": "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddy(ANS2)/RTBuddyService/AppleANS3NVMeController/NS_01@1",
"info_name": "IOService:/AppleARMPE/arm-io@10F00000/AppleT810xIO/ans@77400000/AppleASCWrapV4/iop-ans-nub/RTBuddy(ANS2)/RTBuddyService/AppleANS3NVMeController/NS_01@1",
"type": "nvme",
"protocol": "NVMe"
},
"model_name": "APPLE SSD AP0256Q",
"serial_number": "0ba0147940253c15",
"firmware_version": "555",
"smart_support": {
"available": true,
"enabled": true
},
"smart_status": {
"passed": true,
"nvme": {
"value": 0
}
},
"nvme_smart_health_information_log": {
"critical_warning": 0,
"temperature": 42,
"available_spare": 100,
"available_spare_threshold": 99,
"percentage_used": 1,
"data_units_read": 270189386,
"data_units_written": 166753862,
"host_reads": 7543766995,
"host_writes": 3761621926,
"controller_busy_time": 0,
"power_cycles": 366,
"power_on_hours": 2850,
"unsafe_shutdowns": 195,
"media_errors": 0,
"num_err_log_entries": 0
},
"temperature": {
"current": 42
},
"power_cycle_count": 366,
"power_on_time": {
"hours": 2850
}
}

View File

@@ -4,6 +4,9 @@ import (
"io" "io"
"math" "math"
"os" "os"
"os/exec"
"path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
) )
@@ -86,3 +89,24 @@ func ReadUintFile(path string) (uint64, bool) {
} }
return parsed, true return parsed, true
} }
// LookPathHomebrew is like exec.LookPath but also checks Homebrew paths.
func LookPathHomebrew(file string) (string, error) {
foundPath, lookPathErr := exec.LookPath(file)
if lookPathErr == nil {
return foundPath, nil
}
var homebrewPath string
switch runtime.GOOS {
case "darwin":
homebrewPath = filepath.Join("/opt", "homebrew", "bin", file)
case "linux":
homebrewPath = filepath.Join("/home", "linuxbrew", ".linuxbrew", "bin", file)
}
if homebrewPath != "" {
if _, err := os.Stat(homebrewPath); err == nil {
return homebrewPath, nil
}
}
return "", lookPathErr
}

View File

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

44
go.mod
View File

@@ -5,24 +5,24 @@ go 1.26.1
require ( require (
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/coreos/go-systemd/v22 v22.7.0 github.com/coreos/go-systemd/v22 v22.7.0
github.com/distatus/battery v0.11.0 github.com/ebitengine/purego v0.10.0
github.com/ebitengine/purego v0.9.1
github.com/fxamacker/cbor/v2 v2.9.0 github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lxzan/gws v1.8.9 github.com/lxzan/gws v1.9.1
github.com/nicholas-fedor/shoutrrr v0.13.2 github.com/nicholas-fedor/shoutrrr v0.14.1
github.com/pocketbase/dbx v1.12.0 github.com/pocketbase/dbx v1.12.0
github.com/pocketbase/pocketbase v0.36.4 github.com/pocketbase/pocketbase v0.36.8
github.com/shirou/gopsutil/v4 v4.26.1 github.com/shirou/gopsutil/v4 v4.26.3
github.com/spf13/cast v1.10.0 github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.49.0
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/sys v0.41.0 golang.org/x/sys v0.42.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.1
) )
require ( require (
@@ -30,10 +30,10 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/eclipse/paho.golang v0.23.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
@@ -41,9 +41,10 @@ require (
github.com/go-sql-driver/mysql v1.9.1 // indirect github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
@@ -54,15 +55,14 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/image v0.36.0 // indirect golang.org/x/image v0.38.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/term v0.40.0 // indirect golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
howett.net/plist v1.0.1 // indirect modernc.org/libc v1.70.0 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.45.0 // indirect modernc.org/sqlite v1.48.0 // indirect
) )

114
go.sum
View File

@@ -17,18 +17,16 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distatus/battery v0.11.0 h1:KJk89gz90Iq/wJtbjjM9yUzBXV+ASV/EG2WOOL7N8lc=
github.com/distatus/battery v0.11.0/go.mod h1:KmVkE8A8hpIX4T78QRdMktYpEp35QfOL8A8dwZBxq2k=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/eclipse/paho.golang v0.23.0 h1:KHgl2wz6EJo7cMBmkuhpt7C576vP+kpPv7jjvSyR6Mk=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/eclipse/paho.golang v0.23.0/go.mod h1:nQRhTkoZv8EAiNs5UU0/WdQIx2NrnWUpL9nsGJTQN04=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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/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 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
@@ -58,10 +56,12 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -69,24 +69,24 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= 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/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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 h1:Qj3hTcdWH8uMZDI41HNuTuJN525C7NBrbtH5kSO6fPk=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM= github.com/lxzan/gws v1.9.1 h1:4lbIp4cW0hOLP3ejFHR/uWRy741AURx7oKkNNi2OT9o=
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y= github.com/lxzan/gws v1.9.1/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.13.2 h1:hfsYBIqSFYGg92pZP5CXk/g7/OJIkLYmiUnRl+AD1IA= github.com/nicholas-fedor/shoutrrr v0.14.1 h1:6sx4cJNfNuUtD6ygGlB0dqcCQ+abfsUh+b+6jgujf6A=
github.com/nicholas-fedor/shoutrrr v0.13.2/go.mod h1:ZqzV3gY/Wj6AvWs1etlO7+yKbh4iptSbeL8avBpMQbA= github.com/nicholas-fedor/shoutrrr v0.14.1/go.mod h1:U7IywBkLpBV7rgn8iLbQ9/LklJG1gm24bFv5cXXsDKs=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
@@ -96,8 +96,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA= github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.36.4 h1:zTjRZbp2WfTOJJfb+pFRWa200UaQwxZYt8RzkFMlAZ4= github.com/pocketbase/pocketbase v0.36.8 h1:gCNqoesZ44saYOD3J7edhi5nDwUWKyQG7boM/kVwz2c=
github.com/pocketbase/pocketbase v0.36.4/go.mod h1:9CiezhRudd9FZGa5xZa53QZBTNxc5vvw/FGG+diAECI= github.com/pocketbase/pocketbase v0.36.8/go.mod h1:OY4WaXbP0WnF/EXoBbboWJK+ZSZ1A85tiA0sjrTKxTA=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -105,8 +105,8 @@ 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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= 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/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@@ -115,6 +115,8 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 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/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
@@ -126,44 +128,44 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -175,18 +177,18 @@ howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -195,8 +197,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs= modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -109,6 +109,18 @@ func (am *AlertManager) cancelPendingAlert(alertID string) bool {
return true return true
} }
// CancelPendingStatusAlerts cancels all pending status alert timers for a given system.
// This is called when a system is paused to prevent delayed alerts from firing.
func (am *AlertManager) CancelPendingStatusAlerts(systemID string) {
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
if info.alertData.SystemID == systemID {
am.cancelPendingAlert(key.(string))
}
return true
})
}
// processPendingAlert sends a "down" alert if the pending alert has expired and the system is still down. // processPendingAlert sends a "down" alert if the pending alert has expired and the system is still down.
func (am *AlertManager) processPendingAlert(alertID string) { func (am *AlertManager) processPendingAlert(alertID string) {
value, loaded := am.pendingAlerts.LoadAndDelete(alertID) value, loaded := am.pendingAlerts.LoadAndDelete(alertID)

View File

@@ -941,3 +941,68 @@ func TestStatusAlertClearedBeforeSend(t *testing.T) {
assert.EqualValues(t, 0, alertHistoryCount, "Should have no unresolved alert history records since alert never triggered") assert.EqualValues(t, 0, alertHistoryCount, "Should have no unresolved alert history records since alert never triggered")
}) })
} }
func TestCancelPendingStatusAlertsClearsAllAlertsForSystem(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
userSettings, err := hub.FindFirstRecordByFilter("user_settings", "user={:user}", map[string]any{"user": user.Id})
require.NoError(t, err)
userSettings.Set("settings", `{"emails":["test@example.com"],"webhooks":[]}`)
require.NoError(t, hub.Save(userSettings))
systemCollection, err := hub.FindCollectionByNameOrId("systems")
require.NoError(t, err)
system1 := core.NewRecord(systemCollection)
system1.Set("name", "system-1")
system1.Set("status", "up")
system1.Set("host", "127.0.0.1")
system1.Set("users", []string{user.Id})
require.NoError(t, hub.Save(system1))
system2 := core.NewRecord(systemCollection)
system2.Set("name", "system-2")
system2.Set("status", "up")
system2.Set("host", "127.0.0.2")
system2.Set("users", []string{user.Id})
require.NoError(t, hub.Save(system2))
alertCollection, err := hub.FindCollectionByNameOrId("alerts")
require.NoError(t, err)
alert1 := core.NewRecord(alertCollection)
alert1.Set("user", user.Id)
alert1.Set("system", system1.Id)
alert1.Set("name", "Status")
alert1.Set("triggered", false)
alert1.Set("min", 5)
require.NoError(t, hub.Save(alert1))
alert2 := core.NewRecord(alertCollection)
alert2.Set("user", user.Id)
alert2.Set("system", system2.Id)
alert2.Set("name", "Status")
alert2.Set("triggered", false)
alert2.Set("min", 5)
require.NoError(t, hub.Save(alert2))
am := alerts.NewTestAlertManagerWithoutWorker(hub)
initialEmailCount := hub.TestMailer.TotalSend()
// Both systems go down
require.NoError(t, am.HandleStatusAlerts("down", system1))
require.NoError(t, am.HandleStatusAlerts("down", system2))
assert.Equal(t, 2, am.GetPendingAlertsCount(), "both systems should have pending alerts")
// System 1 is paused — cancel its pending alerts
am.CancelPendingStatusAlerts(system1.Id)
assert.Equal(t, 1, am.GetPendingAlertsCount(), "only system2 alert should remain pending after pausing system1")
// Expire and process remaining alerts — only system2 should fire
am.ForceExpirePendingAlerts()
processed, err := am.ProcessPendingAlerts()
require.NoError(t, err)
assert.Len(t, processed, 1, "only the non-paused system's alert should be processed")
assert.Equal(t, initialEmailCount+1, hub.TestMailer.TotalSend(), "only system2 should send a down notification")
}

View File

@@ -48,6 +48,8 @@ type Stats struct {
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes] MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle] CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle]
CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..] CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..]
DiskIoStats [6]float64 `json:"dios,omitzero" cbor:"35,keyasint,omitzero"` // [read time %, write time %, io utilization %, r_await ms, w_await ms, weighted io %]
MaxDiskIoStats [6]float64 `json:"diosm,omitzero" cbor:"-"` // max values for DiskIoStats
} }
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient. // Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
@@ -97,6 +99,8 @@ type FsStats struct {
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"` DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"` MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"` MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
DiskIoStats [6]float64 `json:"dios,omitzero" cbor:"8,keyasint,omitzero"` // [read time %, write time %, io utilization %, r_await ms, w_await ms, weighted io %]
MaxDiskIoStats [6]float64 `json:"diosm,omitzero" cbor:"-"` // max values for DiskIoStats
} }
type NetIoStats struct { type NetIoStats struct {

View File

@@ -3,6 +3,7 @@ package hub
import ( import (
"context" "context"
"net/http" "net/http"
"regexp"
"strings" "strings"
"time" "time"
@@ -25,6 +26,32 @@ type UpdateInfo struct {
Url string `json:"url"` Url string `json:"url"`
} }
var containerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
// Middleware to allow only admin role users
var requireAdminRole = customAuthMiddleware(func(e *core.RequestEvent) bool {
return e.Auth.GetString("role") == "admin"
})
// Middleware to exclude readonly users
var excludeReadOnlyRole = customAuthMiddleware(func(e *core.RequestEvent) bool {
return e.Auth.GetString("role") != "readonly"
})
// customAuthMiddleware handles boilerplate for custom authentication middlewares. fn should
// return true if the request is allowed, false otherwise. e.Auth is guaranteed to be non-nil.
func customAuthMiddleware(fn func(*core.RequestEvent) bool) func(*core.RequestEvent) error {
return func(e *core.RequestEvent) error {
if e.Auth == nil {
return e.UnauthorizedError("The request requires valid record authorization token.", nil)
}
if !fn(e) {
return e.ForbiddenError("The authorized record is not allowed to perform this action.", nil)
}
return e.Next()
}
}
// registerMiddlewares registers custom middlewares // registerMiddlewares registers custom middlewares
func (h *Hub) registerMiddlewares(se *core.ServeEvent) { func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
// authorizes request with user matching the provided email // authorizes request with user matching the provided email
@@ -33,7 +60,7 @@ func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
return e.Next() return e.Next()
} }
isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost
e.Auth, err = e.App.FindFirstRecordByData("users", "email", email) e.Auth, err = e.App.FindAuthRecordByEmail("users", email)
if err != nil || !isAuthRefresh { if err != nil || !isAuthRefresh {
return e.Next() return e.Next()
} }
@@ -84,19 +111,19 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
// send test notification // send test notification
apiAuth.POST("/test-notification", h.SendTestNotification) apiAuth.POST("/test-notification", h.SendTestNotification)
// heartbeat status and test // heartbeat status and test
apiAuth.GET("/heartbeat-status", h.getHeartbeatStatus) apiAuth.GET("/heartbeat-status", h.getHeartbeatStatus).BindFunc(requireAdminRole)
apiAuth.POST("/test-heartbeat", h.testHeartbeat) apiAuth.POST("/test-heartbeat", h.testHeartbeat).BindFunc(requireAdminRole)
// get config.yml content // get config.yml content
apiAuth.GET("/config-yaml", config.GetYamlConfig) apiAuth.GET("/config-yaml", config.GetYamlConfig).BindFunc(requireAdminRole)
// handle agent websocket connection // handle agent websocket connection
apiNoAuth.GET("/agent-connect", h.handleAgentConnect) apiNoAuth.GET("/agent-connect", h.handleAgentConnect)
// get or create universal tokens // get or create universal tokens
apiAuth.GET("/universal-token", h.getUniversalToken) apiAuth.GET("/universal-token", h.getUniversalToken).BindFunc(excludeReadOnlyRole)
// update / delete user alerts // update / delete user alerts
apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts) apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts)
apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts) apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts)
// refresh SMART devices for a system // refresh SMART devices for a system
apiAuth.POST("/smart/refresh", h.refreshSmartData) apiAuth.POST("/smart/refresh", h.refreshSmartData).BindFunc(excludeReadOnlyRole)
// get systemd service details // get systemd service details
apiAuth.GET("/systemd/info", h.getSystemdInfo) apiAuth.GET("/systemd/info", h.getSystemdInfo)
// /containers routes // /containers routes
@@ -153,6 +180,10 @@ func (info *UpdateInfo) getUpdate(e *core.RequestEvent) error {
// GetUniversalToken handles the universal token API endpoint (create, read, delete) // GetUniversalToken handles the universal token API endpoint (create, read, delete)
func (h *Hub) getUniversalToken(e *core.RequestEvent) error { func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
if e.Auth.IsSuperuser() {
return e.ForbiddenError("Superusers cannot use universal tokens", nil)
}
tokenMap := universalTokenMap.GetMap() tokenMap := universalTokenMap.GetMap()
userID := e.Auth.Id userID := e.Auth.Id
query := e.Request.URL.Query() query := e.Request.URL.Query()
@@ -246,9 +277,6 @@ func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
// getHeartbeatStatus returns current heartbeat configuration and whether it's enabled // getHeartbeatStatus returns current heartbeat configuration and whether it's enabled
func (h *Hub) getHeartbeatStatus(e *core.RequestEvent) error { func (h *Hub) getHeartbeatStatus(e *core.RequestEvent) error {
if e.Auth.GetString("role") != "admin" {
return e.ForbiddenError("Requires admin role", nil)
}
if h.hb == nil { if h.hb == nil {
return e.JSON(http.StatusOK, map[string]any{ return e.JSON(http.StatusOK, map[string]any{
"enabled": false, "enabled": false,
@@ -266,9 +294,6 @@ func (h *Hub) getHeartbeatStatus(e *core.RequestEvent) error {
// testHeartbeat triggers a single heartbeat ping and returns the result // testHeartbeat triggers a single heartbeat ping and returns the result
func (h *Hub) testHeartbeat(e *core.RequestEvent) error { func (h *Hub) testHeartbeat(e *core.RequestEvent) error {
if e.Auth.GetString("role") != "admin" {
return e.ForbiddenError("Requires admin role", nil)
}
if h.hb == nil { if h.hb == nil {
return e.JSON(http.StatusOK, map[string]any{ return e.JSON(http.StatusOK, map[string]any{
"err": "Heartbeat not configured. Set HEARTBEAT_URL environment variable.", "err": "Heartbeat not configured. Set HEARTBEAT_URL environment variable.",
@@ -285,21 +310,18 @@ func (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc func(*syst
systemID := e.Request.URL.Query().Get("system") systemID := e.Request.URL.Query().Get("system")
containerID := e.Request.URL.Query().Get("container") containerID := e.Request.URL.Query().Get("container")
if systemID == "" || containerID == "" { if systemID == "" || containerID == "" || !containerIDPattern.MatchString(containerID) {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and container parameters are required"}) return e.BadRequestError("Invalid system or container parameter", nil)
}
if !containerIDPattern.MatchString(containerID) {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "invalid container parameter"})
} }
system, err := h.sm.GetSystem(systemID) system, err := h.sm.GetSystem(systemID)
if err != nil { if err != nil || !system.HasUser(e.App, e.Auth.Id) {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"}) return e.NotFoundError("", nil)
} }
data, err := fetchFunc(system, containerID) data, err := fetchFunc(system, containerID)
if err != nil { if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": err.Error()}) return e.InternalServerError("", err)
} }
return e.JSON(http.StatusOK, map[string]string{responseKey: data}) return e.JSON(http.StatusOK, map[string]string{responseKey: data})
@@ -325,15 +347,23 @@ func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
serviceName := query.Get("service") serviceName := query.Get("service")
if systemID == "" || serviceName == "" { if systemID == "" || serviceName == "" {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and service parameters are required"}) return e.BadRequestError("Invalid system or service parameter", nil)
} }
system, err := h.sm.GetSystem(systemID) system, err := h.sm.GetSystem(systemID)
if err != nil || !system.HasUser(e.App, e.Auth.Id) {
return e.NotFoundError("", nil)
}
// verify service exists before fetching details
_, err = e.App.FindFirstRecordByFilter("systemd_services", "system = {:system} && name = {:name}", dbx.Params{
"system": systemID,
"name": serviceName,
})
if err != nil { if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"}) return e.NotFoundError("", err)
} }
details, err := system.FetchSystemdInfoFromAgent(serviceName) details, err := system.FetchSystemdInfoFromAgent(serviceName)
if err != nil { if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": err.Error()}) return e.InternalServerError("", err)
} }
e.Response.Header().Set("Cache-Control", "public, max-age=60") e.Response.Header().Set("Cache-Control", "public, max-age=60")
return e.JSON(http.StatusOK, map[string]any{"details": details}) return e.JSON(http.StatusOK, map[string]any{"details": details})
@@ -344,17 +374,16 @@ func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
func (h *Hub) refreshSmartData(e *core.RequestEvent) error { func (h *Hub) refreshSmartData(e *core.RequestEvent) error {
systemID := e.Request.URL.Query().Get("system") systemID := e.Request.URL.Query().Get("system")
if systemID == "" { if systemID == "" {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system parameter is required"}) return e.BadRequestError("Invalid system parameter", nil)
} }
system, err := h.sm.GetSystem(systemID) system, err := h.sm.GetSystem(systemID)
if err != nil { if err != nil || !system.HasUser(e.App, e.Auth.Id) {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"}) return e.NotFoundError("", nil)
} }
// Fetch and save SMART devices
if err := system.FetchAndSaveSmartDevices(); err != nil { if err := system.FetchAndSaveSmartDevices(); err != nil {
return e.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return e.InternalServerError("", err)
} }
return e.JSON(http.StatusOK, map[string]string{"status": "ok"}) return e.JSON(http.StatusOK, map[string]string{"status": "ok"})

View File

@@ -3,6 +3,7 @@ package hub_test
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"testing" "testing"
@@ -25,33 +26,33 @@ func jsonReader(v any) io.Reader {
} }
func TestApiRoutesAuthentication(t *testing.T) { func TestApiRoutesAuthentication(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir()) hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup() defer hub.Cleanup()
hub.StartHub()
// Create test user and get auth token
user, err := beszelTests.CreateUser(hub, "testuser@example.com", "password123")
require.NoError(t, err, "Failed to create test user")
adminUser, err := beszelTests.CreateRecord(hub, "users", map[string]any{
"email": "admin@example.com",
"password": "password123",
"role": "admin",
})
require.NoError(t, err, "Failed to create admin user")
adminUserToken, err := adminUser.NewAuthToken()
// superUser, err := beszelTests.CreateRecord(hub, core.CollectionNameSuperusers, map[string]any{
// "email": "superuser@example.com",
// "password": "password123",
// })
// require.NoError(t, err, "Failed to create superuser")
userToken, err := user.NewAuthToken() userToken, err := user.NewAuthToken()
require.NoError(t, err, "Failed to create auth token") require.NoError(t, err, "Failed to create auth token")
// Create test system for user-alerts endpoints // Create test user and get auth token
user2, err := beszelTests.CreateUser(hub, "testuser@example.com", "password123")
require.NoError(t, err, "Failed to create test user")
user2Token, err := user2.NewAuthToken()
require.NoError(t, err, "Failed to create user2 auth token")
adminUser, err := beszelTests.CreateUserWithRole(hub, "admin@example.com", "password123", "admin")
require.NoError(t, err, "Failed to create admin user")
adminUserToken, err := adminUser.NewAuthToken()
readOnlyUser, err := beszelTests.CreateUserWithRole(hub, "readonly@example.com", "password123", "readonly")
require.NoError(t, err, "Failed to create readonly user")
readOnlyUserToken, err := readOnlyUser.NewAuthToken()
require.NoError(t, err, "Failed to create readonly user auth token")
superuser, err := beszelTests.CreateSuperuser(hub, "superuser@example.com", "password123")
require.NoError(t, err, "Failed to create superuser")
superuserToken, err := superuser.NewAuthToken()
require.NoError(t, err, "Failed to create superuser auth token")
// Create test system
system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{ system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "test-system", "name": "test-system",
"users": []string{user.Id}, "users": []string{user.Id},
@@ -106,7 +107,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 403, ExpectedStatus: 403,
ExpectedContent: []string{"Requires admin"}, ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -136,7 +137,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 403, ExpectedStatus: 403,
ExpectedContent: []string{"Requires admin role"}, ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -158,7 +159,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 403, ExpectedStatus: 403,
ExpectedContent: []string{"Requires admin role"}, ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -202,6 +203,74 @@ func TestApiRoutesAuthentication(t *testing.T) {
ExpectedContent: []string{"\"permanent\":true", "permanent-token-123"}, ExpectedContent: []string{"\"permanent\":true", "permanent-token-123"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{
Name: "GET /universal-token - superuser should fail",
Method: http.MethodGet,
URL: "/api/beszel/universal-token",
Headers: map[string]string{
"Authorization": superuserToken,
},
ExpectedStatus: 403,
ExpectedContent: []string{"Superusers cannot use universal tokens"},
TestAppFactory: func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
},
},
{
Name: "GET /universal-token - with readonly auth should fail",
Method: http.MethodGet,
URL: "/api/beszel/universal-token",
Headers: map[string]string{
"Authorization": readOnlyUserToken,
},
ExpectedStatus: 403,
ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory,
},
{
Name: "POST /smart/refresh - missing system should fail 400 with user auth",
Method: http.MethodPost,
URL: "/api/beszel/smart/refresh",
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"Invalid", "system", "parameter"},
TestAppFactory: testAppFactory,
},
{
Name: "POST /smart/refresh - with readonly auth should fail",
Method: http.MethodPost,
URL: fmt.Sprintf("/api/beszel/smart/refresh?system=%s", system.Id),
Headers: map[string]string{
"Authorization": readOnlyUserToken,
},
ExpectedStatus: 403,
ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory,
},
{
Name: "POST /smart/refresh - non-user system should fail",
Method: http.MethodPost,
URL: fmt.Sprintf("/api/beszel/smart/refresh?system=%s", system.Id),
Headers: map[string]string{
"Authorization": user2Token,
},
ExpectedStatus: 404,
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
},
{
Name: "POST /smart/refresh - good user should pass validation",
Method: http.MethodPost,
URL: fmt.Sprintf("/api/beszel/smart/refresh?system=%s", system.Id),
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 500,
ExpectedContent: []string{"Something went wrong while processing your request."},
TestAppFactory: testAppFactory,
},
{ {
Name: "POST /user-alerts - no auth should fail", Name: "POST /user-alerts - no auth should fail",
Method: http.MethodPost, Method: http.MethodPost,
@@ -273,20 +342,42 @@ func TestApiRoutesAuthentication(t *testing.T) {
{ {
Name: "GET /containers/logs - no auth should fail", Name: "GET /containers/logs - no auth should fail",
Method: http.MethodGet, Method: http.MethodGet,
URL: "/api/beszel/containers/logs?system=test-system&container=test-container", URL: "/api/beszel/containers/logs?system=test-system&container=abababababab",
ExpectedStatus: 401, ExpectedStatus: 401,
ExpectedContent: []string{"requires valid"}, ExpectedContent: []string{"requires valid"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{
Name: "GET /containers/logs - request for valid non-user system should fail",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/containers/logs?system=%s&container=abababababab", system.Id),
ExpectedStatus: 404,
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
Headers: map[string]string{
"Authorization": user2Token,
},
},
{
Name: "GET /containers/info - request for valid non-user system should fail",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/containers/info?system=%s&container=abababababab", system.Id),
ExpectedStatus: 404,
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
Headers: map[string]string{
"Authorization": user2Token,
},
},
{ {
Name: "GET /containers/logs - with auth but missing system param should fail", Name: "GET /containers/logs - with auth but missing system param should fail",
Method: http.MethodGet, Method: http.MethodGet,
URL: "/api/beszel/containers/logs?container=test-container", URL: "/api/beszel/containers/logs?container=abababababab",
Headers: map[string]string{ Headers: map[string]string{
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
ExpectedContent: []string{"system and container parameters are required"}, ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -297,7 +388,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
ExpectedContent: []string{"system and container parameters are required"}, ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -308,7 +399,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 404, ExpectedStatus: 404,
ExpectedContent: []string{"system not found"}, ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -319,7 +410,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
ExpectedContent: []string{"invalid container parameter"}, ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -330,7 +421,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
ExpectedContent: []string{"invalid container parameter"}, ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{ {
@@ -341,9 +432,114 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken, "Authorization": userToken,
}, },
ExpectedStatus: 400, ExpectedStatus: 400,
ExpectedContent: []string{"invalid container parameter"}, ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory, TestAppFactory: testAppFactory,
}, },
{
Name: "GET /containers/logs - good user should pass validation",
Method: http.MethodGet,
URL: "/api/beszel/containers/logs?system=" + system.Id + "&container=0123456789ab",
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 500,
ExpectedContent: []string{"Something went wrong while processing your request."},
TestAppFactory: testAppFactory,
},
{
Name: "GET /containers/info - good user should pass validation",
Method: http.MethodGet,
URL: "/api/beszel/containers/info?system=" + system.Id + "&container=0123456789ab",
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 500,
ExpectedContent: []string{"Something went wrong while processing your request."},
TestAppFactory: testAppFactory,
},
// /systemd routes
{
Name: "GET /systemd/info - no auth should fail",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=nginx.service", system.Id),
ExpectedStatus: 401,
ExpectedContent: []string{"requires valid"},
TestAppFactory: testAppFactory,
},
{
Name: "GET /systemd/info - request for valid non-user system should fail",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=nginx.service", system.Id),
ExpectedStatus: 404,
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
Headers: map[string]string{
"Authorization": user2Token,
},
},
{
Name: "GET /systemd/info - with auth but missing system param should fail",
Method: http.MethodGet,
URL: "/api/beszel/systemd/info?service=nginx.service",
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory,
},
{
Name: "GET /systemd/info - with auth but missing service param should fail",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s", system.Id),
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory,
},
{
Name: "GET /systemd/info - with auth but invalid system should fail",
Method: http.MethodGet,
URL: "/api/beszel/systemd/info?system=invalid-system&service=nginx.service",
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 404,
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
},
{
Name: "GET /systemd/info - service not in systemd_services collection should fail",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=notregistered.service", system.Id),
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 404,
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
},
{
Name: "GET /systemd/info - with auth and existing service record should pass validation",
Method: http.MethodGet,
URL: fmt.Sprintf("/api/beszel/systemd/info?system=%s&service=nginx.service", system.Id),
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 500,
ExpectedContent: []string{"Something went wrong while processing your request."},
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
beszelTests.CreateRecord(app, "systemd_services", map[string]any{
"system": system.Id,
"name": "nginx.service",
"state": 0,
"sub": 1,
})
},
},
// Auth Optional Routes - Should work without authentication // Auth Optional Routes - Should work without authentication
{ {
@@ -434,13 +630,17 @@ func TestApiRoutesAuthentication(t *testing.T) {
"systems": []string{system.Id}, "systems": []string{system.Id},
}), }),
}, },
{ // this works but diff behavior on prod vs dev.
Name: "GET /update - shouldn't exist without CHECK_UPDATES env var", // dev returns 502; prod returns 200 with static html page 404
Method: http.MethodGet, // TODO: align dev and prod behavior and re-enable this test
URL: "/api/beszel/update", // {
ExpectedStatus: 502, // Name: "GET /update - shouldn't exist without CHECK_UPDATES env var",
TestAppFactory: testAppFactory, // Method: http.MethodGet,
}, // URL: "/api/beszel/update",
// NotExpectedContent: []string{"v:", "\"v\":"},
// ExpectedStatus: 502,
// TestAppFactory: testAppFactory,
// },
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {

View File

@@ -279,9 +279,6 @@ func createFingerprintRecord(app core.App, systemID, token string) error {
// Returns the current config.yml file as a JSON object // Returns the current config.yml file as a JSON object
func GetYamlConfig(e *core.RequestEvent) error { func GetYamlConfig(e *core.RequestEvent) error {
if e.Auth.GetString("role") != "admin" {
return e.ForbiddenError("Requires admin role", nil)
}
configContent, err := generateYAML(e.App) configContent, err := generateYAML(e.App)
if err != nil { if err != nil {
return err return err

View File

@@ -9,7 +9,6 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"regexp"
"strings" "strings"
"github.com/henrygd/beszel/internal/alerts" "github.com/henrygd/beszel/internal/alerts"
@@ -38,8 +37,6 @@ type Hub struct {
appURL string appURL string
} }
var containerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
// NewHub creates a new Hub instance with default configuration // NewHub creates a new Hub instance with default configuration
func NewHub(app core.App) *Hub { func NewHub(app core.App) *Hub {
hub := &Hub{App: app} hub := &Hub{App: app}

View File

@@ -5,7 +5,6 @@ package hub
import ( import (
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@@ -62,7 +61,6 @@ func (rm *responseModifier) modifyHTML(html string) string {
// startServer sets up the development server for Beszel // startServer sets up the development server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error { func (h *Hub) startServer(se *core.ServeEvent) error {
slog.Info("starting server", "appURL", h.appURL)
proxy := httputil.NewSingleHostReverseProxy(&url.URL{ proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http", Scheme: "http",
Host: "localhost:5173", Host: "localhost:5173",

View File

@@ -8,6 +8,7 @@ import (
"hash/fnv" "hash/fnv"
"math/rand" "math/rand"
"net" "net"
"slices"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -145,6 +146,7 @@ func (sys *System) update() error {
// update smart interval if it's set on the agent side // update smart interval if it's set on the agent side
if data.Details.SmartInterval > 0 { if data.Details.SmartInterval > 0 {
sys.smartInterval = data.Details.SmartInterval sys.smartInterval = data.Details.SmartInterval
sys.manager.hub.Logger().Info("SMART interval updated from agent details", "system", sys.Id, "interval", sys.smartInterval.String())
// make sure we reset expiration of lastFetch to remain as long as the new smart interval // make sure we reset expiration of lastFetch to remain as long as the new smart interval
// to prevent premature expiration leading to new fetch if interval is different. // to prevent premature expiration leading to new fetch if interval is different.
sys.manager.smartFetchMap.UpdateExpiration(sys.Id, sys.smartInterval+time.Minute) sys.manager.smartFetchMap.UpdateExpiration(sys.Id, sys.smartInterval+time.Minute)
@@ -156,11 +158,10 @@ func (sys *System) update() error {
if sys.smartInterval <= 0 { if sys.smartInterval <= 0 {
sys.smartInterval = time.Hour sys.smartInterval = time.Hour
} }
lastFetch, _ := sys.manager.smartFetchMap.GetOk(sys.Id) if sys.shouldFetchSmart() && sys.smartFetching.CompareAndSwap(false, true) {
if time.Since(time.UnixMilli(lastFetch-1e4)) >= sys.smartInterval && sys.smartFetching.CompareAndSwap(false, true) { sys.manager.hub.Logger().Info("SMART fetch", "system", sys.Id, "interval", sys.smartInterval.String())
go func() { go func() {
defer sys.smartFetching.Store(false) defer sys.smartFetching.Store(false)
sys.manager.smartFetchMap.Set(sys.Id, time.Now().UnixMilli(), sys.smartInterval+time.Minute)
_ = sys.FetchAndSaveSmartDevices() _ = sys.FetchAndSaveSmartDevices()
}() }()
} }
@@ -184,7 +185,7 @@ func (sys *System) handlePaused() {
// createRecords updates the system record and adds system_stats and container_stats records // createRecords updates the system record and adds system_stats and container_stats records
func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) { func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {
systemRecord, err := sys.getRecord() systemRecord, err := sys.getRecord(sys.manager.hub)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -343,8 +344,8 @@ func createContainerRecords(app core.App, data []*container.Stats, systemId stri
// getRecord retrieves the system record from the database. // getRecord retrieves the system record from the database.
// If the record is not found, it removes the system from the manager. // If the record is not found, it removes the system from the manager.
func (sys *System) getRecord() (*core.Record, error) { func (sys *System) getRecord(app core.App) (*core.Record, error) {
record, err := sys.manager.hub.FindRecordById("systems", sys.Id) record, err := app.FindRecordById("systems", sys.Id)
if err != nil || record == nil { if err != nil || record == nil {
_ = sys.manager.RemoveSystem(sys.Id) _ = sys.manager.RemoveSystem(sys.Id)
return nil, err return nil, err
@@ -352,6 +353,16 @@ func (sys *System) getRecord() (*core.Record, error) {
return record, nil return record, nil
} }
// HasUser checks if the given user ID is in the system's users list.
func (sys *System) HasUser(app core.App, userID string) bool {
record, err := sys.getRecord(app)
if err != nil {
return false
}
users := record.GetStringSlice("users")
return slices.Contains(users, userID)
}
// setDown marks a system as down in the database. // setDown marks a system as down in the database.
// It takes the original error that caused the system to go down and returns any error // It takes the original error that caused the system to go down and returns any error
// encountered during the process of updating the system status. // encountered during the process of updating the system status.
@@ -359,7 +370,7 @@ func (sys *System) setDown(originalError error) error {
if sys.Status == down || sys.Status == paused { if sys.Status == down || sys.Status == paused {
return nil return nil
} }
record, err := sys.getRecord() record, err := sys.getRecord(sys.manager.hub)
if err != nil { if err != nil {
return err return err
} }
@@ -643,6 +654,7 @@ func (s *System) createSSHClient() error {
return err return err
} }
s.agentVersion, _ = extractAgentVersion(string(s.client.Conn.ServerVersion())) s.agentVersion, _ = extractAgentVersion(string(s.client.Conn.ServerVersion()))
s.manager.resetFailedSmartFetchState(s.Id)
return nil return nil
} }

View File

@@ -44,7 +44,7 @@ type SystemManager struct {
hub hubLike // Hub interface for database and alert operations hub hubLike // Hub interface for database and alert operations
systems *store.Store[string, *System] // Thread-safe store of active systems systems *store.Store[string, *System] // Thread-safe store of active systems
sshConfig *ssh.ClientConfig // SSH client configuration for system connections sshConfig *ssh.ClientConfig // SSH client configuration for system connections
smartFetchMap *expirymap.ExpiryMap[int64] // Stores last SMART fetch time per system ID smartFetchMap *expirymap.ExpiryMap[smartFetchState] // Stores last SMART fetch time/result; TTL is only for cleanup
} }
// hubLike defines the interface requirements for the hub dependency. // hubLike defines the interface requirements for the hub dependency.
@@ -54,6 +54,7 @@ type hubLike interface {
GetSSHKey(dataDir string) (ssh.Signer, error) GetSSHKey(dataDir string) (ssh.Signer, error)
HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error
HandleStatusAlerts(status string, systemRecord *core.Record) error HandleStatusAlerts(status string, systemRecord *core.Record) error
CancelPendingStatusAlerts(systemID string)
} }
// NewSystemManager creates a new SystemManager instance with the provided hub. // NewSystemManager creates a new SystemManager instance with the provided hub.
@@ -62,7 +63,7 @@ func NewSystemManager(hub hubLike) *SystemManager {
return &SystemManager{ return &SystemManager{
systems: store.New(map[string]*System{}), systems: store.New(map[string]*System{}),
hub: hub, hub: hub,
smartFetchMap: expirymap.New[int64](time.Hour), smartFetchMap: expirymap.New[smartFetchState](time.Hour),
} }
} }
@@ -189,6 +190,7 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
system.closeSSHConnection() system.closeSSHConnection()
} }
_ = deactivateAlerts(e.App, e.Record.Id) _ = deactivateAlerts(e.App, e.Record.Id)
sm.hub.CancelPendingStatusAlerts(e.Record.Id)
return e.Next() return e.Next()
case pending: case pending:
// Resume monitoring, preferring existing WebSocket connection // Resume monitoring, preferring existing WebSocket connection
@@ -306,6 +308,7 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
if err != nil { if err != nil {
return err return err
} }
sm.resetFailedSmartFetchState(systemId)
system := sm.NewSystem(systemId) system := sm.NewSystem(systemId)
system.WsConn = wsConn system.WsConn = wsConn
@@ -317,6 +320,15 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
return nil return nil
} }
// resetFailedSmartFetchState clears only failed SMART cooldown entries so a fresh
// agent reconnect retries SMART discovery immediately after configuration changes.
func (sm *SystemManager) resetFailedSmartFetchState(systemID string) {
state, ok := sm.smartFetchMap.GetOk(systemID)
if ok && !state.Successful {
sm.smartFetchMap.Remove(systemID)
}
}
// createSSHClientConfig initializes the SSH client configuration for connecting to an agent's server // createSSHClientConfig initializes the SSH client configuration for connecting to an agent's server
func (sm *SystemManager) createSSHClientConfig() error { func (sm *SystemManager) createSSHClientConfig() error {
privateKey, err := sm.hub.GetSSHKey("") privateKey, err := sm.hub.GetSSHKey("")

View File

@@ -19,7 +19,6 @@ type subscriptionInfo struct {
var ( var (
activeSubscriptions = make(map[string]*subscriptionInfo) activeSubscriptions = make(map[string]*subscriptionInfo)
workerRunning bool workerRunning bool
realtimeTicker *time.Ticker
tickerStopChan chan struct{} tickerStopChan chan struct{}
realtimeMutex sync.Mutex realtimeMutex sync.Mutex
) )
@@ -70,7 +69,7 @@ func (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubscribeReq
} }
// onRealtimeSubscriptionAdded initializes or starts the realtime worker when the first subscription is added. // onRealtimeSubscriptionAdded initializes or starts the realtime worker when the first subscription is added.
// It ensures only one worker runs at a time and creates the ticker for periodic data fetching. // It ensures only one worker runs at a time.
func (sm *SystemManager) onRealtimeSubscriptionAdded() { func (sm *SystemManager) onRealtimeSubscriptionAdded() {
realtimeMutex.Lock() realtimeMutex.Lock()
defer realtimeMutex.Unlock() defer realtimeMutex.Unlock()
@@ -82,11 +81,6 @@ func (sm *SystemManager) onRealtimeSubscriptionAdded() {
tickerStopChan = make(chan struct{}) tickerStopChan = make(chan struct{})
go sm.startRealtimeWorker() go sm.startRealtimeWorker()
} }
// If no ticker exists, create one
if realtimeTicker == nil {
realtimeTicker = time.NewTicker(1 * time.Second)
}
} }
// checkSubscriptions stops the realtime worker when there are no active subscriptions. // checkSubscriptions stops the realtime worker when there are no active subscriptions.
@@ -107,11 +101,6 @@ func (sm *SystemManager) checkSubscriptions() {
} }
} }
if realtimeTicker != nil {
realtimeTicker.Stop()
realtimeTicker = nil
}
// Mark worker as stopped (will be reset when next subscription comes in) // Mark worker as stopped (will be reset when next subscription comes in)
workerRunning = false workerRunning = false
} }
@@ -135,17 +124,16 @@ func (sm *SystemManager) removeRealtimeSubscription(subscription string, options
// It continuously fetches system data and broadcasts it to subscribed clients via WebSocket. // It continuously fetches system data and broadcasts it to subscribed clients via WebSocket.
func (sm *SystemManager) startRealtimeWorker() { func (sm *SystemManager) startRealtimeWorker() {
sm.fetchRealtimeDataAndNotify() sm.fetchRealtimeDataAndNotify()
tick := time.Tick(1 * time.Second)
for { for {
select { select {
case <-tickerStopChan: case <-tickerStopChan:
return return
case <-realtimeTicker.C: case <-tick:
// Check if ticker is still valid (might have been stopped) if len(activeSubscriptions) == 0 {
if realtimeTicker == nil || len(activeSubscriptions) == 0 {
return return
} }
// slog.Debug("activeSubscriptions", "count", len(activeSubscriptions))
sm.fetchRealtimeDataAndNotify() sm.fetchRealtimeDataAndNotify()
} }
} }

View File

@@ -4,18 +4,61 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"strings" "strings"
"time"
"github.com/henrygd/beszel/internal/entities/smart" "github.com/henrygd/beszel/internal/entities/smart"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
) )
type smartFetchState struct {
LastAttempt int64
Successful bool
}
// FetchAndSaveSmartDevices fetches SMART data from the agent and saves it to the database // FetchAndSaveSmartDevices fetches SMART data from the agent and saves it to the database
func (sys *System) FetchAndSaveSmartDevices() error { func (sys *System) FetchAndSaveSmartDevices() error {
smartData, err := sys.FetchSmartDataFromAgent() smartData, err := sys.FetchSmartDataFromAgent()
if err != nil || len(smartData) == 0 { if err != nil {
sys.recordSmartFetchResult(err, 0)
return err return err
} }
return sys.saveSmartDevices(smartData) err = sys.saveSmartDevices(smartData)
sys.recordSmartFetchResult(err, len(smartData))
return err
}
// recordSmartFetchResult stores a cooldown entry for the SMART interval and marks
// whether the last fetch produced any devices, so failed setup can retry on reconnect.
func (sys *System) recordSmartFetchResult(err error, deviceCount int) {
if sys.manager == nil {
return
}
interval := sys.smartFetchInterval()
success := err == nil && deviceCount > 0
if sys.manager.hub != nil {
sys.manager.hub.Logger().Info("SMART fetch result", "system", sys.Id, "success", success, "devices", deviceCount, "interval", interval.String(), "err", err)
}
sys.manager.smartFetchMap.Set(sys.Id, smartFetchState{LastAttempt: time.Now().UnixMilli(), Successful: success}, interval+time.Minute)
}
// shouldFetchSmart returns true when there is no active SMART cooldown entry for this system.
func (sys *System) shouldFetchSmart() bool {
if sys.manager == nil {
return true
}
state, ok := sys.manager.smartFetchMap.GetOk(sys.Id)
if !ok {
return true
}
return !time.UnixMilli(state.LastAttempt).Add(sys.smartFetchInterval()).After(time.Now())
}
// smartFetchInterval returns the agent-provided SMART interval or the default when unset.
func (sys *System) smartFetchInterval() time.Duration {
if sys.smartInterval > 0 {
return sys.smartInterval
}
return time.Hour
} }
// saveSmartDevices saves SMART device data to the smart_devices collection // saveSmartDevices saves SMART device data to the smart_devices collection

View File

@@ -0,0 +1,94 @@
//go:build testing
package systems
import (
"errors"
"testing"
"time"
"github.com/henrygd/beszel/internal/hub/expirymap"
"github.com/stretchr/testify/assert"
)
func TestRecordSmartFetchResult(t *testing.T) {
sm := &SystemManager{smartFetchMap: expirymap.New[smartFetchState](time.Hour)}
t.Cleanup(sm.smartFetchMap.StopCleaner)
sys := &System{
Id: "system-1",
manager: sm,
smartInterval: time.Hour,
}
// Successful fetch with devices
sys.recordSmartFetchResult(nil, 5)
state, ok := sm.smartFetchMap.GetOk(sys.Id)
assert.True(t, ok, "expected smart fetch result to be stored")
assert.True(t, state.Successful, "expected successful fetch state to be recorded")
// Failed fetch
sys.recordSmartFetchResult(errors.New("failed"), 0)
state, ok = sm.smartFetchMap.GetOk(sys.Id)
assert.True(t, ok, "expected failed smart fetch state to be stored")
assert.False(t, state.Successful, "expected failed smart fetch state to be marked unsuccessful")
// Successful fetch but no devices
sys.recordSmartFetchResult(nil, 0)
state, ok = sm.smartFetchMap.GetOk(sys.Id)
assert.True(t, ok, "expected fetch with zero devices to be stored")
assert.False(t, state.Successful, "expected fetch with zero devices to be marked unsuccessful")
}
func TestShouldFetchSmart(t *testing.T) {
sm := &SystemManager{smartFetchMap: expirymap.New[smartFetchState](time.Hour)}
t.Cleanup(sm.smartFetchMap.StopCleaner)
sys := &System{
Id: "system-1",
manager: sm,
smartInterval: time.Hour,
}
assert.True(t, sys.shouldFetchSmart(), "expected initial smart fetch to be allowed")
sys.recordSmartFetchResult(errors.New("failed"), 0)
assert.False(t, sys.shouldFetchSmart(), "expected smart fetch to be blocked while interval entry exists")
sm.smartFetchMap.Remove(sys.Id)
assert.True(t, sys.shouldFetchSmart(), "expected smart fetch to be allowed after interval entry is cleared")
}
func TestShouldFetchSmart_IgnoresExtendedTTLWhenFetchIsDue(t *testing.T) {
sm := &SystemManager{smartFetchMap: expirymap.New[smartFetchState](time.Hour)}
t.Cleanup(sm.smartFetchMap.StopCleaner)
sys := &System{
Id: "system-1",
manager: sm,
smartInterval: time.Hour,
}
sm.smartFetchMap.Set(sys.Id, smartFetchState{
LastAttempt: time.Now().Add(-2 * time.Hour).UnixMilli(),
Successful: true,
}, 10*time.Minute)
sm.smartFetchMap.UpdateExpiration(sys.Id, 3*time.Hour)
assert.True(t, sys.shouldFetchSmart(), "expected fetch time to take precedence over updated TTL")
}
func TestResetFailedSmartFetchState(t *testing.T) {
sm := &SystemManager{smartFetchMap: expirymap.New[smartFetchState](time.Hour)}
t.Cleanup(sm.smartFetchMap.StopCleaner)
sm.smartFetchMap.Set("system-1", smartFetchState{LastAttempt: time.Now().UnixMilli(), Successful: false}, time.Hour)
sm.resetFailedSmartFetchState("system-1")
_, ok := sm.smartFetchMap.GetOk("system-1")
assert.False(t, ok, "expected failed smart fetch state to be cleared on reconnect")
sm.smartFetchMap.Set("system-1", smartFetchState{LastAttempt: time.Now().UnixMilli(), Successful: true}, time.Hour)
sm.resetFailedSmartFetchState("system-1")
_, ok = sm.smartFetchMap.GetOk("system-1")
assert.True(t, ok, "expected successful smart fetch state to be preserved")
}

View File

@@ -111,6 +111,9 @@ func (ws *WsConn) Close(msg []byte) {
// Ping sends a ping frame to keep the connection alive. // Ping sends a ping frame to keep the connection alive.
func (ws *WsConn) Ping() error { func (ws *WsConn) Ping() error {
if ws.conn == nil {
return gws.ErrConnClosed
}
ws.conn.SetDeadline(time.Now().Add(deadline)) ws.conn.SetDeadline(time.Now().Add(deadline))
return ws.conn.WritePing(nil) return ws.conn.WritePing(nil)
} }

View File

@@ -230,6 +230,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Bandwidth[1] += stats.Bandwidth[1] sum.Bandwidth[1] += stats.Bandwidth[1]
sum.DiskIO[0] += stats.DiskIO[0] sum.DiskIO[0] += stats.DiskIO[0]
sum.DiskIO[1] += stats.DiskIO[1] sum.DiskIO[1] += stats.DiskIO[1]
for i := range stats.DiskIoStats {
sum.DiskIoStats[i] += stats.DiskIoStats[i]
}
batterySum += int(stats.Battery[0]) batterySum += int(stats.Battery[0])
sum.Battery[1] = stats.Battery[1] sum.Battery[1] = stats.Battery[1]
@@ -254,6 +257,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1]) sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
sum.MaxDiskIO[0] = max(sum.MaxDiskIO[0], stats.MaxDiskIO[0], stats.DiskIO[0]) sum.MaxDiskIO[0] = max(sum.MaxDiskIO[0], stats.MaxDiskIO[0], stats.DiskIO[0])
sum.MaxDiskIO[1] = max(sum.MaxDiskIO[1], stats.MaxDiskIO[1], stats.DiskIO[1]) sum.MaxDiskIO[1] = max(sum.MaxDiskIO[1], stats.MaxDiskIO[1], stats.DiskIO[1])
for i := range stats.DiskIoStats {
sum.MaxDiskIoStats[i] = max(sum.MaxDiskIoStats[i], stats.MaxDiskIoStats[i], stats.DiskIoStats[i])
}
// Accumulate network interfaces // Accumulate network interfaces
if sum.NetworkInterfaces == nil { if sum.NetworkInterfaces == nil {
@@ -299,6 +305,10 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
fs.DiskWriteBytes += value.DiskWriteBytes fs.DiskWriteBytes += value.DiskWriteBytes
fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes) fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)
fs.MaxDiskWriteBytes = max(fs.MaxDiskWriteBytes, value.MaxDiskWriteBytes, value.DiskWriteBytes) fs.MaxDiskWriteBytes = max(fs.MaxDiskWriteBytes, value.MaxDiskWriteBytes, value.DiskWriteBytes)
for i := range value.DiskIoStats {
fs.DiskIoStats[i] += value.DiskIoStats[i]
fs.MaxDiskIoStats[i] = max(fs.MaxDiskIoStats[i], value.MaxDiskIoStats[i], value.DiskIoStats[i])
}
} }
} }
@@ -350,6 +360,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count) sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
sum.DiskIO[0] = sum.DiskIO[0] / uint64(count) sum.DiskIO[0] = sum.DiskIO[0] / uint64(count)
sum.DiskIO[1] = sum.DiskIO[1] / uint64(count) sum.DiskIO[1] = sum.DiskIO[1] / uint64(count)
for i := range sum.DiskIoStats {
sum.DiskIoStats[i] = twoDecimals(sum.DiskIoStats[i] / count)
}
sum.NetworkSent = twoDecimals(sum.NetworkSent / count) sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count) sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count) sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
@@ -388,6 +401,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
fs.DiskReadPs = twoDecimals(fs.DiskReadPs / count) fs.DiskReadPs = twoDecimals(fs.DiskReadPs / count)
fs.DiskReadBytes = fs.DiskReadBytes / uint64(count) fs.DiskReadBytes = fs.DiskReadBytes / uint64(count)
fs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count) fs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count)
for i := range fs.DiskIoStats {
fs.DiskIoStats[i] = twoDecimals(fs.DiskIoStats[i] / count)
}
} }
} }

View File

@@ -41,7 +41,7 @@
"recharts": "^2.15.4", "recharts": "^2.15.4",
"shiki": "^3.13.0", "shiki": "^3.13.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"valibot": "^0.42.1", "valibot": "^1.3.1",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.2.4", "@biomejs/biome": "2.2.4",
@@ -927,7 +927,7 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"valibot": ["valibot@0.42.1", "", { "peerDependencies": { "typescript": ">=5" } }, "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw=="], "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="manifest" href="./static/manifest.json" crossorigin="use-credentials" /> <link rel="manifest" href="./static/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/svg+xml" href="./static/icon.svg" /> <link rel="icon" type="image/svg+xml" href="./static/icon.svg" />
<link rel="apple-touch-icon" href="./static/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<title>Beszel</title> <title>Beszel</title>

View File

@@ -1,12 +1,12 @@
{ {
"name": "beszel", "name": "beszel",
"version": "0.18.3", "version": "0.18.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "beszel", "name": "beszel",
"version": "0.18.3", "version": "0.18.7",
"dependencies": { "dependencies": {
"@henrygd/queue": "^1.0.7", "@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2", "@henrygd/semaphore": "^0.0.2",
@@ -44,7 +44,7 @@
"recharts": "^2.15.4", "recharts": "^2.15.4",
"shiki": "^3.13.0", "shiki": "^3.13.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"valibot": "^0.42.1" "valibot": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.2.4", "@biomejs/biome": "2.2.4",
@@ -986,29 +986,6 @@
"integrity": "sha512-N3W7MKwTRmAxOjeG0NAT18oe2Xn3KdjkpMR6crbkF1UDamMGPjyigqEsefiv+qTaxibtc1a+zXCVzb9YXANVqw==", "integrity": "sha512-N3W7MKwTRmAxOjeG0NAT18oe2Xn3KdjkpMR6crbkF1UDamMGPjyigqEsefiv+qTaxibtc1a+zXCVzb9YXANVqw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1243,9 +1220,9 @@
} }
}, },
"node_modules/@lingui/cli/node_modules/picomatch": { "node_modules/@lingui/cli/node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2408,9 +2385,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw==", "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2422,9 +2399,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ==", "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2436,9 +2413,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==", "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2450,9 +2427,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ==", "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2464,9 +2441,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ==", "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2478,9 +2455,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ==", "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2492,9 +2469,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ==", "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2506,9 +2483,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q==", "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2520,9 +2497,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ==", "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2534,9 +2511,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ==", "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2547,10 +2524,24 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ==", "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -2562,9 +2553,23 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ==", "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2576,9 +2581,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw==", "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -2590,9 +2595,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA==", "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -2604,9 +2609,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"integrity": "sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ==", "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -2618,9 +2623,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA==", "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2632,9 +2637,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg==", "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2645,10 +2650,38 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ==", "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2660,9 +2693,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ==", "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -2673,10 +2706,24 @@
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.48.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
"integrity": "sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg==", "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3235,6 +3282,66 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.5",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.4",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.5",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.4",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.0",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
@@ -3589,9 +3696,9 @@
} }
}, },
"node_modules/anymatch/node_modules/picomatch": { "node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3620,6 +3727,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3666,6 +3783,19 @@
"readable-stream": "^3.4.0" "readable-stream": "^3.4.0"
} }
}, },
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -5072,9 +5202,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.sortby": { "node_modules/lodash.sortby": {
@@ -5267,9 +5397,9 @@
} }
}, },
"node_modules/micromatch/node_modules/picomatch": { "node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5290,16 +5420,16 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.1.1", "version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/brace-expansion": "^5.0.0" "brace-expansion": "^5.0.5"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -5575,9 +5705,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5956,9 +6086,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.48.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5972,26 +6102,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.48.1", "@rollup/rollup-android-arm-eabi": "4.60.1",
"@rollup/rollup-android-arm64": "4.48.1", "@rollup/rollup-android-arm64": "4.60.1",
"@rollup/rollup-darwin-arm64": "4.48.1", "@rollup/rollup-darwin-arm64": "4.60.1",
"@rollup/rollup-darwin-x64": "4.48.1", "@rollup/rollup-darwin-x64": "4.60.1",
"@rollup/rollup-freebsd-arm64": "4.48.1", "@rollup/rollup-freebsd-arm64": "4.60.1",
"@rollup/rollup-freebsd-x64": "4.48.1", "@rollup/rollup-freebsd-x64": "4.60.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.48.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
"@rollup/rollup-linux-arm-musleabihf": "4.48.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
"@rollup/rollup-linux-arm64-gnu": "4.48.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1",
"@rollup/rollup-linux-arm64-musl": "4.48.1", "@rollup/rollup-linux-arm64-musl": "4.60.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.48.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1",
"@rollup/rollup-linux-ppc64-gnu": "4.48.1", "@rollup/rollup-linux-loong64-musl": "4.60.1",
"@rollup/rollup-linux-riscv64-gnu": "4.48.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
"@rollup/rollup-linux-riscv64-musl": "4.48.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1",
"@rollup/rollup-linux-s390x-gnu": "4.48.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
"@rollup/rollup-linux-x64-gnu": "4.48.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1",
"@rollup/rollup-linux-x64-musl": "4.48.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.48.1", "@rollup/rollup-linux-x64-gnu": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.48.1", "@rollup/rollup-linux-x64-musl": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.48.1", "@rollup/rollup-openbsd-x64": "4.60.1",
"@rollup/rollup-openharmony-arm64": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.60.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -6290,9 +6425,9 @@
} }
}, },
"node_modules/tar": { "node_modules/tar": {
"version": "7.5.7", "version": "7.5.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
@@ -6559,9 +6694,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/valibot": { "node_modules/valibot": {
"version": "0.42.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-0.42.1.tgz", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.3.1.tgz",
"integrity": "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==", "integrity": "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"typescript": ">=5" "typescript": ">=5"

View File

@@ -1,7 +1,7 @@
{ {
"name": "beszel", "name": "beszel",
"private": true, "private": true,
"version": "0.18.4", "version": "0.18.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
@@ -52,7 +52,7 @@
"recharts": "^2.15.4", "recharts": "^2.15.4",
"shiki": "^3.13.0", "shiki": "^3.13.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"valibot": "^0.42.1" "valibot": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.2.4", "@biomejs/biome": "2.2.4",

View File

@@ -1,8 +1,8 @@
import { msg, t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react" import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react" import { memo, useEffect, useRef, useState } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
@@ -12,7 +12,6 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@@ -35,28 +34,19 @@ import { DropdownMenu, DropdownMenuTrigger } from "./ui/dropdown-menu"
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons" import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons"
import { InputCopy } from "./ui/input-copy" import { InputCopy } from "./ui/input-copy"
export function AddSystemButton({ className }: { className?: string }) { // To avoid a refactor of the dialog, we will just keep this function as a "skeleton" for the actual dialog
if (isReadOnlyUser()) { export function AddSystemDialog({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
return null
}
const [open, setOpen] = useState(false)
const opened = useRef(false) const opened = useRef(false)
if (open) { if (open) {
opened.current = true opened.current = true
} }
if (isReadOnlyUser()) {
return null
}
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" className={cn("flex gap-1 max-xs:h-[2.4rem]", className)}>
<PlusIcon className="h-4 w-4 450:-ms-1" />
<span className="hidden 450:inline">
<Trans>
Add <span className="hidden sm:inline">System</span>
</Trans>
</span>
</Button>
</DialogTrigger>
{opened.current && <SystemDialog setOpen={setOpen} />} {opened.current && <SystemDialog setOpen={setOpen} />}
</Dialog> </Dialog>
) )
@@ -276,7 +266,13 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
/> />
</TabsContent> </TabsContent>
{/* Save */} {/* Save */}
<Button>{system ? <Trans>Save system</Trans> : <Trans>Add system</Trans>}</Button> <Button>
{system ? (
<Trans>Save {{ foo: systemTranslation }}</Trans>
) : (
<Trans>Add {{ foo: systemTranslation }}</Trans>
)}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Tabs> </Tabs>

View File

@@ -20,7 +20,7 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
<SheetTrigger asChild> <SheetTrigger asChild>
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}> <Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
<BellIcon <BellIcon
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", { className={cn("size-[1.2em] pointer-events-none", {
"fill-primary": hasSystemAlert, "fill-primary": hasSystemAlert,
})} })}
/> />

View File

@@ -2,11 +2,13 @@ import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro" import { Plural, Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { GlobeIcon, ServerIcon } from "lucide-react" import { ChevronDownIcon, GlobeIcon, ServerIcon } from "lucide-react"
import { lazy, memo, Suspense, useMemo, useState } from "react" import { lazy, memo, Suspense, useMemo, useState } from "react"
import { $router, Link } from "@/components/router" import { $router, Link } from "@/components/router"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
@@ -64,11 +66,57 @@ const deleteAlerts = debounce(async ({ name, systems }: { name: string; systems:
export const AlertDialogContent = memo(function AlertDialogContent({ system }: { system: SystemRecord }) { export const AlertDialogContent = memo(function AlertDialogContent({ system }: { system: SystemRecord }) {
const alerts = useStore($alerts) const alerts = useStore($alerts)
const systems = useStore($systems)
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false) const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
const [currentTab, setCurrentTab] = useState("system") const [currentTab, setCurrentTab] = useState("system")
// copyKey is used to force remount AlertContent components with
// new alert data after copying alerts from another system
const [copyKey, setCopyKey] = useState(0)
const systemAlerts = alerts[system.id] ?? new Map() const systemAlerts = alerts[system.id] ?? new Map()
// Systems that have at least one alert configured (excluding the current system)
const systemsWithAlerts = useMemo(
() => systems.filter((s) => s.id !== system.id && alerts[s.id]?.size),
[systems, alerts, system.id]
)
async function copyAlertsFromSystem(sourceSystemId: string) {
const sourceAlerts = $alerts.get()[sourceSystemId]
if (!sourceAlerts?.size) return
try {
const currentTargetAlerts = $alerts.get()[system.id] ?? new Map()
// Alert names present on target but absent from source should be deleted
const namesToDelete = Array.from(currentTargetAlerts.keys()).filter((name) => !sourceAlerts.has(name))
await Promise.all([
...Array.from(sourceAlerts.values()).map(({ name, value, min }) =>
pb.send<{ success: boolean }>(endpoint, {
method: "POST",
body: { name, value, min, systems: [system.id], overwrite: true },
requestKey: name,
})
),
...namesToDelete.map((name) =>
pb.send<{ success: boolean }>(endpoint, {
method: "DELETE",
body: { name, systems: [system.id] },
requestKey: name,
})
),
])
// Optimistically update the store so components re-mount with correct data
// before the realtime subscription event arrives.
const newSystemAlerts = new Map<string, AlertRecord>()
for (const alert of sourceAlerts.values()) {
newSystemAlerts.set(alert.name, { ...alert, system: system.id, triggered: false })
}
$alerts.setKey(system.id, newSystemAlerts)
setCopyKey((k) => k + 1)
} catch (error) {
failedUpdateToast(error)
}
}
// We need to keep a copy of alerts when we switch to global tab. If we always compare to // We need to keep a copy of alerts when we switch to global tab. If we always compare to
// current alerts, it will only be updated when first checked, then won't be updated because // current alerts, it will only be updated when first checked, then won't be updated because
// after that it exists. // after that it exists.
@@ -93,7 +141,8 @@ export const AlertDialogContent = memo(function AlertDialogContent({ system }: {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Tabs defaultValue="system" onValueChange={setCurrentTab}> <Tabs defaultValue="system" onValueChange={setCurrentTab}>
<TabsList className="mb-1 -mt-0.5"> <div className="flex items-center justify-between mb-1 -mt-0.5">
<TabsList>
<TabsTrigger value="system"> <TabsTrigger value="system">
<ServerIcon className="me-2 h-3.5 w-3.5" /> <ServerIcon className="me-2 h-3.5 w-3.5" />
<span className="truncate max-w-60">{system.name}</span> <span className="truncate max-w-60">{system.name}</span>
@@ -103,8 +152,26 @@ export const AlertDialogContent = memo(function AlertDialogContent({ system }: {
<Trans>All Systems</Trans> <Trans>All Systems</Trans>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
{systemsWithAlerts.length > 0 && currentTab === "system" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="text-muted-foreground text-xs gap-1.5">
<Trans context="Copy alerts from another system">Copy from</Trans>
<ChevronDownIcon className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="max-h-100 overflow-auto">
{systemsWithAlerts.map((s) => (
<DropdownMenuItem key={s.id} className="min-w-44" onSelect={() => copyAlertsFromSystem(s.id)}>
{s.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<TabsContent value="system"> <TabsContent value="system">
<div className="grid gap-3"> <div key={copyKey} className="grid gap-3">
{alertKeys.map((name) => ( {alertKeys.map((name) => (
<AlertContent <AlertContent
key={name} key={name}

View File

@@ -1,4 +1,4 @@
import { useMemo } from "react" import { type ReactNode, useEffect, useMemo, useState } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { import {
ChartContainer, ChartContainer,
@@ -11,18 +11,23 @@ import {
import { chartMargin, cn, formatShortDate } from "@/lib/utils" import { chartMargin, cn, formatShortDate } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types" import type { ChartData, SystemStatsRecord } from "@/types"
import { useYAxisWidth } from "./hooks" import { useYAxisWidth } from "./hooks"
import { AxisDomain } from "recharts/types/util/types" import type { AxisDomain } from "recharts/types/util/types"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
export type DataPoint = { export type DataPoint<T = SystemStatsRecord> = {
label: string label: string
dataKey: (data: SystemStatsRecord) => number | undefined dataKey: (data: T) => number | null | undefined
color: number | string color: number | string
opacity: number opacity: number
stackId?: string | number stackId?: string | number
order?: number
strokeOpacity?: number
activeDot?: boolean
} }
export default function AreaChartDefault({ export default function AreaChartDefault({
chartData, chartData,
customData,
max, max,
maxToggled, maxToggled,
tickFormatter, tickFormatter,
@@ -34,35 +39,88 @@ export default function AreaChartDefault({
showTotal = false, showTotal = false,
reverseStackOrder = false, reverseStackOrder = false,
hideYAxis = false, hideYAxis = false,
}: // logRender = false, filter,
{ truncate = false,
chartProps,
}: {
chartData: ChartData chartData: ChartData
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
customData?: any[]
max?: number max?: number
maxToggled?: boolean maxToggled?: boolean
tickFormatter: (value: number, index: number) => string tickFormatter: (value: number, index: number) => string
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string // biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item interop
dataPoints?: DataPoint[] contentFormatter: (item: any, key: string) => ReactNode
// biome-ignore lint/suspicious/noExplicitAny: accepts DataPoint with different generic types
dataPoints?: DataPoint<any>[]
domain?: AxisDomain domain?: AxisDomain
legend?: boolean legend?: boolean
showTotal?: boolean showTotal?: boolean
// biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item interop
itemSorter?: (a: any, b: any) => number itemSorter?: (a: any, b: any) => number
reverseStackOrder?: boolean reverseStackOrder?: boolean
hideYAxis?: boolean hideYAxis?: boolean
// logRender?: boolean filter?: string
}) { truncate?: boolean
chartProps?: Omit<React.ComponentProps<typeof AreaChart>, "data" | "margin">
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
const sourceData = customData ?? chartData.systemStats
const [displayData, setDisplayData] = useState(sourceData)
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
// Reduce chart redraws by only updating while visible or when chart time changes
useEffect(() => {
const shouldPrimeData = sourceData.length && !displayData.length
const sourceChanged = sourceData !== displayData
const shouldUpdate = shouldPrimeData || (sourceChanged && isIntersecting)
if (shouldUpdate) {
setDisplayData(sourceData)
}
if (isIntersecting && maxToggled !== displayMaxToggled) {
setDisplayMaxToggled(maxToggled)
}
}, [displayData, displayMaxToggled, isIntersecting, maxToggled, sourceData])
// Use a stable key derived from data point identities and visual properties
const areasKey = dataPoints?.map((d) => `${d.label}:${d.opacity}`).join("\0")
const Areas = useMemo(() => {
return dataPoints?.map((dataPoint, i) => {
let { color } = dataPoint
if (typeof color === "number") {
color = `var(--chart-${color})`
}
return (
<Area
key={dataPoint.label}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
fill={color}
fillOpacity={dataPoint.opacity}
stroke={color}
strokeOpacity={dataPoint.strokeOpacity}
isAnimationActive={false}
stackId={dataPoint.stackId}
order={dataPoint.order || i}
activeDot={dataPoint.activeDot ?? true}
/>
)
})
}, [areasKey, displayMaxToggled])
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
return useMemo(() => { return useMemo(() => {
if (chartData.systemStats.length === 0) { if (displayData.length === 0) {
return null return null
} }
// if (logRender) { // if (logRender) {
// console.log("Rendered at", new Date()) // console.log("Rendered", dataPoints?.map((d) => d.label).join(", "), new Date())
// } // }
return ( return (
<div>
<ChartContainer <ChartContainer
ref={ref}
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", { className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth || hideYAxis, "opacity-100": yAxisWidth || hideYAxis,
"ps-4": hideYAxis, "ps-4": hideYAxis,
@@ -71,8 +129,9 @@ export default function AreaChartDefault({
<AreaChart <AreaChart
reverseStackOrder={reverseStackOrder} reverseStackOrder={reverseStackOrder}
accessibilityLayer accessibilityLayer
data={chartData.systemStats} data={displayData}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin} margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
{...chartProps}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
{!hideYAxis && ( {!hideYAxis && (
@@ -98,32 +157,15 @@ export default function AreaChartDefault({
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={contentFormatter} contentFormatter={contentFormatter}
showTotal={showTotal} showTotal={showTotal}
filter={filter}
truncate={truncate}
/> />
} }
/> />
{dataPoints?.map((dataPoint) => { {Areas}
let { color } = dataPoint {legend && <ChartLegend content={<ChartLegendContent />} />}
if (typeof color === "number") {
color = `var(--chart-${color})`
}
return (
<Area
key={dataPoint.label}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
fill={color}
fillOpacity={dataPoint.opacity}
stroke={color}
isAnimationActive={false}
stackId={dataPoint.stackId}
/>
)
})}
{legend && <ChartLegend content={<ChartLegendContent reverse={reverseStackOrder} />} />}
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>
</div>
) )
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled, showTotal]) }, [displayData, yAxisWidth, filter, Areas])
} }

View File

@@ -1,215 +0,0 @@
// 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,
pinnedAxisDomain,
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 { useYAxisWidth } from "./hooks"
export default memo(function ContainerChart({
dataKey,
chartData,
chartType,
chartConfig,
unit = "%",
}: {
dataKey: string
chartData: ChartData
chartType: ChartType
chartConfig: ChartConfig
unit?: string
}) {
const filter = useStore($containerFilter)
const userSettings = useStore($userSettings)
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { containerData } = chartData
const isNetChart = chartType === ChartType.Network
// Filter with set lookup
const filteredKeys = useMemo(() => {
if (!filter) {
return new Set<string>()
}
const filterTerms = filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
return new Set(
Object.keys(chartConfig).filter((key) => {
const keyLower = key.toLowerCase()
return !filterTerms.some((term) => keyLower.includes(term))
})
)
}, [chartConfig, filter])
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
const obj = {} as {
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
dataFunction: (key: string, data: any) => number | null
tickFormatter: (value: any) => string
}
// tick formatter
if (chartType === ChartType.CPU) {
obj.tickFormatter = (value) => {
const val = `${toFixedFloat(value, 2)}%`
return updateYAxisWidth(val)
}
} else {
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
obj.tickFormatter = (val) => {
const { value, unit } = formatBytes(val, isNetChart, chartUnit, !isNetChart)
return updateYAxisWidth(`${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`)
}
}
// tooltip formatter
if (isNetChart) {
const getRxTxBytes = (record?: { b?: [number, number]; ns?: number; nr?: number }) => {
if (record?.b?.length && record.b.length >= 2) {
return [Number(record.b[0]) || 0, Number(record.b[1]) || 0]
}
return [(record?.ns ?? 0) * 1024 * 1024, (record?.nr ?? 0) * 1024 * 1024]
}
const formatRxTx = (recv: number, sent: number) => {
const { value: receivedValue, unit: receivedUnit } = formatBytes(recv, true, userSettings.unitNet, false)
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, false)
return (
<span className="flex">
{decimalString(receivedValue)} {receivedUnit}
<span className="opacity-70 ms-0.5"> rx </span>
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
{decimalString(sentValue)} {sentUnit}
<span className="opacity-70 ms-0.5"> tx</span>
</span>
)
}
obj.toolTipFormatter = (item: any, key: string) => {
try {
if (key === "__total__") {
let totalSent = 0
let totalRecv = 0
const payloadData = item?.payload && typeof item.payload === "object" ? item.payload : {}
for (const [containerKey, value] of Object.entries(payloadData)) {
if (!value || typeof value !== "object") {
continue
}
// Skip filtered out containers
if (filteredKeys.has(containerKey)) {
continue
}
const [sent, recv] = getRxTxBytes(value as { b?: [number, number]; ns?: number; nr?: number })
totalSent += sent
totalRecv += recv
}
return formatRxTx(totalRecv, totalSent)
}
const [sent, recv] = getRxTxBytes(item?.payload?.[key])
return formatRxTx(recv, sent)
} catch (e) {
return null
}
}
} else if (chartType === ChartType.Memory) {
obj.toolTipFormatter = (item: any) => {
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
return `${decimalString(value)} ${unit}`
}
} else {
obj.toolTipFormatter = (item: any) => `${decimalString(item.value)}${unit}`
}
// data function
if (isNetChart) {
obj.dataFunction = (key: string, data: any) => {
const payload = data[key]
if (!payload) {
return null
}
const sent = payload?.b?.[0] ?? (payload?.ns ?? 0) * 1024 * 1024
const recv = payload?.b?.[1] ?? (payload?.nr ?? 0) * 1024 * 1024
return sent + recv
}
} else {
obj.dataFunction = (key: string, data: any) => data[key]?.[dataKey] ?? null
}
return obj
}, [filteredKeys])
// console.log('rendered at', new Date())
if (containerData.length === 0) {
return null
}
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<AreaChart
accessibilityLayer
// syncId={'cpu'}
data={containerData}
margin={chartMargin}
reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
domain={pinnedAxisDomain()}
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
tickFormatter={tickFormatter}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
truncate={true}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} showTotal={true} />}
/>
{Object.keys(chartConfig).map((key) => {
const filtered = filteredKeys.has(key)
const fillOpacity = filtered ? 0.05 : 0.4
const strokeOpacity = filtered ? 0.1 : 1
return (
<Area
key={key}
isAnimationActive={false}
dataKey={dataFunction.bind(null, key)}
name={key}
type="monotoneX"
fill={chartConfig[key].color}
fillOpacity={fillOpacity}
stroke={chartConfig[key].color}
strokeOpacity={strokeOpacity}
activeDot={{ opacity: filtered ? 0 : 1 }}
stackId="a"
/>
)
})}
</AreaChart>
</ChartContainer>
</div>
)
})

View File

@@ -1,83 +0,0 @@
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 { Unit } from "@/lib/enums"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { useYAxisWidth } from "./hooks"
export default memo(function DiskChart({
dataKey,
diskSize,
chartData,
}: {
dataKey: string | ((data: SystemStatsRecord) => number | undefined)
diskSize: number
chartData: ChartData
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { t } = useLingui()
// round to nearest GB
if (diskSize >= 100) {
diskSize = Math.round(diskSize)
}
if (chartData.systemStats.length === 0) {
return null
}
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
domain={[0, diskSize]}
tickCount={9}
minTickGap={6}
tickLine={false}
axisLine={false}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
}}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return decimalString(convertedValue) + " " + unit
}}
/>
}
/>
<Area
dataKey={dataKey}
name={t`Disk Usage`}
type="monotoneX"
fill="var(--chart-4)"
fillOpacity={0.4}
stroke="var(--chart-4)"
// animationDuration={1200}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</div>
)
})

View File

@@ -1,117 +0,0 @@
import { memo, useMemo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
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
}
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<LineChart accessibilityLayer data={gpuData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
domain={[0, "auto"]}
width={yAxisWidth}
tickFormatter={(value) => {
const val = toFixedFloat(value, 2)
return updateYAxisWidth(`${val}W`)
}}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @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`}
// indicator="line"
/>
}
/>
{dataPoints.map((dataPoint) => (
<Line
key={dataPoint.label}
dataKey={dataPoint.dataKey}
name={dataPoint.label}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={dataPoint.color as string}
isAnimationActive={false}
/>
))}
{dataPoints.length > 1 && <ChartLegend content={<ChartLegendContent />} />}
</LineChart>
</ChartContainer>
</div>
)
})

View File

@@ -1,6 +1,9 @@
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { useStore } from "@nanostores/react"
import type { ChartConfig } from "@/components/ui/chart" import type { ChartConfig } from "@/components/ui/chart"
import type { ChartData, SystemStats, SystemStatsRecord } from "@/types" import type { ChartData, SystemStats, SystemStatsRecord } from "@/types"
import type { DataPoint } from "./area-chart"
import { $containerFilter } from "@/lib/stores"
/** Chart configurations for CPU, memory, and network usage charts */ /** Chart configurations for CPU, memory, and network usage charts */
export interface ContainerChartConfigs { export interface ContainerChartConfigs {
@@ -96,9 +99,9 @@ export function useYAxisWidth() {
clearTimeout(timeout) clearTimeout(timeout)
timeout = setTimeout(() => { timeout = setTimeout(() => {
document.body.appendChild(div) document.body.appendChild(div)
const width = div.offsetWidth + 24 const width = div.offsetWidth + 20
if (width > yAxisWidth) { if (width > yAxisWidth) {
setYAxisWidth(div.offsetWidth + 24) setYAxisWidth(width)
} }
document.body.removeChild(div) document.body.removeChild(div)
}) })
@@ -108,6 +111,44 @@ export function useYAxisWidth() {
return { yAxisWidth, updateYAxisWidth } return { yAxisWidth, updateYAxisWidth }
} }
/** Subscribes to the container filter store and returns filtered DataPoints for container charts */
export function useContainerDataPoints(
chartConfig: ChartConfig,
// biome-ignore lint/suspicious/noExplicitAny: container data records have dynamic keys
dataFn: (key: string, data: Record<string, any>) => number | null
) {
const filter = useStore($containerFilter)
const { dataPoints, filteredKeys } = useMemo(() => {
const filterTerms = filter
? filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
: []
const filtered = new Set<string>()
const points = Object.keys(chartConfig).map((key) => {
const isFiltered = filterTerms.length > 0 && !filterTerms.some((term) => key.toLowerCase().includes(term))
if (isFiltered) filtered.add(key)
return {
label: key,
// biome-ignore lint/suspicious/noExplicitAny: container data records have dynamic keys
dataKey: (data: Record<string, any>) => dataFn(key, data),
color: chartConfig[key].color ?? "",
opacity: isFiltered ? 0.05 : 0.4,
strokeOpacity: isFiltered ? 0.1 : 1,
activeDot: !isFiltered,
stackId: "a",
}
})
return {
// biome-ignore lint/suspicious/noExplicitAny: container data records have dynamic keys
dataPoints: points as DataPoint<Record<string, any>>[],
filteredKeys: filtered,
}
}, [chartConfig, filter])
return { filter, dataPoints, filteredKeys }
}
// Assures consistent colors for network interfaces // Assures consistent colors for network interfaces
export function useNetworkInterfaces(interfaces: SystemStats["ni"]) { export function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
const keys = Object.keys(interfaces ?? {}) const keys = Object.keys(interfaces ?? {})

View File

@@ -1,4 +1,4 @@
import { useMemo } from "react" import { type ReactNode, useEffect, useMemo, useState } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts" import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import { import {
ChartContainer, ChartContainer,
@@ -11,15 +11,22 @@ import {
import { chartMargin, cn, formatShortDate } from "@/lib/utils" import { chartMargin, cn, formatShortDate } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types" import type { ChartData, SystemStatsRecord } from "@/types"
import { useYAxisWidth } from "./hooks" import { useYAxisWidth } from "./hooks"
import type { AxisDomain } from "recharts/types/util/types"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
export type DataPoint = { export type DataPoint<T = SystemStatsRecord> = {
label: string label: string
dataKey: (data: SystemStatsRecord) => number | undefined dataKey: (data: T) => number | null | undefined
color: number | string color: number | string
stackId?: string | number
order?: number
strokeOpacity?: number
activeDot?: boolean
} }
export default function LineChartDefault({ export default function LineChartDefault({
chartData, chartData,
customData,
max, max,
maxToggled, maxToggled,
tickFormatter, tickFormatter,
@@ -28,62 +35,58 @@ export default function LineChartDefault({
domain, domain,
legend, legend,
itemSorter, itemSorter,
}: // logRender = false, showTotal = false,
{ reverseStackOrder = false,
hideYAxis = false,
filter,
truncate = false,
chartProps,
}: {
chartData: ChartData chartData: ChartData
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
customData?: any[]
max?: number max?: number
maxToggled?: boolean maxToggled?: boolean
tickFormatter: (value: number, index: number) => string tickFormatter: (value: number, index: number) => string
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string // biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item interop
dataPoints?: DataPoint[] contentFormatter: (item: any, key: string) => ReactNode
domain?: [number, number] // biome-ignore lint/suspicious/noExplicitAny: accepts DataPoint with different generic types
dataPoints?: DataPoint<any>[]
domain?: AxisDomain
legend?: boolean legend?: boolean
showTotal?: boolean
// biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item interop
itemSorter?: (a: any, b: any) => number itemSorter?: (a: any, b: any) => number
// logRender?: boolean reverseStackOrder?: boolean
hideYAxis?: boolean
filter?: string
truncate?: boolean
chartProps?: Omit<React.ComponentProps<typeof LineChart>, "data" | "margin">
}) { }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
const sourceData = customData ?? chartData.systemStats
const [displayData, setDisplayData] = useState(sourceData)
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore // Reduce chart redraws by only updating while visible or when chart time changes
return useMemo(() => { useEffect(() => {
if (chartData.systemStats.length === 0) { const shouldPrimeData = sourceData.length && !displayData.length
return null const sourceChanged = sourceData !== displayData
const shouldUpdate = shouldPrimeData || (sourceChanged && isIntersecting)
if (shouldUpdate) {
setDisplayData(sourceData)
} }
// if (logRender) { if (isIntersecting && maxToggled !== displayMaxToggled) {
// console.log("Rendered at", new Date()) setDisplayMaxToggled(maxToggled)
// }
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}
/>
} }
/> }, [displayData, displayMaxToggled, isIntersecting, maxToggled, sourceData])
{dataPoints?.map((dataPoint) => {
// Use a stable key derived from data point identities and visual properties
const linesKey = dataPoints?.map((d) => `${d.label}:${d.strokeOpacity ?? ""}`).join("\0")
const Lines = useMemo(() => {
return dataPoints?.map((dataPoint, i) => {
let { color } = dataPoint let { color } = dataPoint
if (typeof color === "number") { if (typeof color === "number") {
color = `var(--chart-${color})` color = `var(--chart-${color})`
@@ -97,14 +100,71 @@ export default function LineChartDefault({
dot={false} dot={false}
strokeWidth={1.5} strokeWidth={1.5}
stroke={color} stroke={color}
strokeOpacity={dataPoint.strokeOpacity}
isAnimationActive={false} isAnimationActive={false}
// stackId={dataPoint.stackId}
order={dataPoint.order || i}
// activeDot={dataPoint.activeDot ?? true}
/> />
) )
})
}, [linesKey, displayMaxToggled])
return useMemo(() => {
if (displayData.length === 0) {
return null
}
// if (logRender) {
// console.log("Rendered", dataPoints?.map((d) => d.label).join(", "), new Date())
// }
return (
<ChartContainer
ref={ref}
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth || hideYAxis,
"ps-4": hideYAxis,
})} })}
>
<LineChart
reverseStackOrder={reverseStackOrder}
accessibilityLayer
data={displayData}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
{...chartProps}
>
<CartesianGrid vertical={false} />
{!hideYAxis && (
<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}
showTotal={showTotal}
filter={filter}
truncate={truncate}
/>
}
/>
{Lines}
{legend && <ChartLegend content={<ChartLegendContent />} />} {legend && <ChartLegend content={<ChartLegendContent />} />}
</LineChart> </LineChart>
</ChartContainer> </ChartContainer>
</div>
) )
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled]) }, [displayData, yAxisWidth, filter, Lines])
} }

View File

@@ -1,83 +0,0 @@
import { t } from "@lingui/core/macro"
import { memo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
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 }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const keys: { color: string; label: string }[] = [
{
color: "hsl(271, 81%, 60%)", // Purple
label: t({ message: `1 min`, comment: "Load average" }),
},
{
color: "hsl(217, 91%, 60%)", // Blue
label: t({ message: `5 min`, comment: "Load average" }),
},
{
color: "hsl(25, 95%, 53%)", // Orange
label: t({ message: `15 min`, comment: "Load average" }),
},
]
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"
domain={[0, "auto"]}
width={yAxisWidth}
tickFormatter={(value) => {
return updateYAxisWidth(String(toFixedFloat(value, 2)))
}}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => decimalString(item.value)}
/>
}
/>
{keys.map(({ color, label }, i) => (
<Line
key={label}
dataKey={(value: { stats: SystemStats }) => value.stats?.la?.[i]}
name={label}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={color}
isAnimationActive={false}
/>
))}
<ChartLegend content={<ChartLegendContent />} />
</LineChart>
</ChartContainer>
</div>
)
})

View File

@@ -1,108 +0,0 @@
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 { 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 }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { t } = useLingui()
const totalMem = toFixedFloat(chartData.systemStats.at(-1)?.stats.m ?? 0, 1)
// console.log('rendered at', new Date())
if (chartData.systemStats.length === 0) {
return null
}
return (
<div>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
<CartesianGrid vertical={false} />
{totalMem && (
<YAxis
direction="ltr"
orientation={chartData.orientation}
// use "ticks" instead of domain / tickcount if need more control
domain={[0, totalMem]}
tickCount={9}
className="tracking-tighter"
width={yAxisWidth}
tickLine={false}
axisLine={false}
tickFormatter={(value) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
}}
/>
)}
{xAxis(chartData)}
<ChartTooltip
// cursor={false}
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
// @ts-expect-error
itemSorter={(a, b) => a.order - b.order}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={({ value }) => {
// mem values are supplied as GB
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
}}
showTotal={true}
/>
}
/>
<Area
name={t`Used`}
order={3}
dataKey={({ stats }) => (showMax ? stats?.mm : stats?.mu)}
type="monotoneX"
fill="var(--chart-2)"
fillOpacity={0.4}
stroke="var(--chart-2)"
stackId="1"
isAnimationActive={false}
/>
{/* {chartData.systemStats.at(-1)?.stats.mz && ( */}
<Area
name="ZFS ARC"
order={2}
dataKey={({ stats }) => (showMax ? null : stats?.mz)}
type="monotoneX"
fill="hsla(175 60% 45% / 0.8)"
fillOpacity={0.5}
stroke="hsla(175 60% 45% / 0.8)"
stackId="1"
isAnimationActive={false}
/>
{/* )} */}
<Area
name={t`Cache / Buffers`}
order={1}
dataKey={({ stats }) => (showMax ? null : stats?.mb)}
type="monotoneX"
fill="hsla(160 60% 45% / 0.5)"
fillOpacity={0.4}
stroke="hsla(160 60% 45% / 0.5)"
stackId="1"
isAnimationActive={false}
/>
{/* <ChartLegend content={<ChartLegendContent />} /> */}
</AreaChart>
</ChartContainer>
</div>
)
})

View File

@@ -1,70 +0,0 @@
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 { $userSettings } from "@/lib/stores"
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 }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const userSettings = useStore($userSettings)
if (chartData.systemStats.length === 0) {
return null
}
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
domain={[0, () => toFixedFloat(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
width={yAxisWidth}
tickLine={false}
axisLine={false}
tickFormatter={(value) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
}}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={({ value }) => {
// mem values are supplied as GB
const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
}}
// indicator="line"
/>
}
/>
<Area
dataKey="stats.su"
name={t`Used`}
type="monotoneX"
fill="var(--chart-2)"
fillOpacity={0.4}
stroke="var(--chart-2)"
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</div>
)
})

View File

@@ -1,117 +0,0 @@
import { useStore } from "@nanostores/react"
import { memo, useMemo } from "react"
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { $temperatureFilter, $userSettings } from "@/lib/stores"
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 }) {
const filter = useStore($temperatureFilter)
const userSettings = useStore($userSettings)
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
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 tempSums = {} as Record<string, number>
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++) {
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 (const key of keys) {
newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
}
return newChartData
}, [chartData])
const colors = Object.keys(newChartData.colors)
// console.log('rendered at', new Date())
return (
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
domain={["auto", "auto"]}
width={yAxisWidth}
tickFormatter={(val) => {
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
return updateYAxisWidth(toFixedFloat(value, 2) + " " + unit)
}}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => {
const { value, unit } = formatTemperature(item.value, userSettings.unitTemp)
return decimalString(value) + " " + unit
}}
filter={filter}
/>
}
/>
{colors.map((key) => {
const filterTerms = filter ? filter.toLowerCase().split(" ").filter(term => term.length > 0) : []
const filtered = filterTerms.length > 0 && !filterTerms.some(term => key.toLowerCase().includes(term))
const strokeOpacity = filtered ? 0.1 : 1
return (
<Line
key={key}
dataKey={key}
name={key}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={newChartData.colors[key]}
strokeOpacity={strokeOpacity}
activeDot={{ opacity: filtered ? 0 : 1 }}
isAnimationActive={false}
/>
)
})}
{colors.length < 12 && <ChartLegend content={<ChartLegendContent />} />}
</LineChart>
</ChartContainer>
</div>
)
})

View File

@@ -163,9 +163,9 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
const visibleColumns = table.getVisibleLeafColumns() const visibleColumns = table.getVisibleLeafColumns()
return ( return (
<Card className="p-6 @container w-full"> <Card className="@container w-full px-3 py-5 sm:py-6 sm:px-6">
<CardHeader className="p-0 mb-4"> <CardHeader className="p-0 mb-3 sm:mb-4">
<div className="grid md:flex gap-5 w-full items-end"> <div className="grid md:flex gap-x-5 gap-y-3 w-full items-end">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2"> <CardTitle className="mb-2">
<Trans>All Containers</Trans> <Trans>All Containers</Trans>
@@ -462,7 +462,6 @@ function ContainerSheet({
function ContainersTableHead({ table }: { table: TableType<ContainerRecord> }) { function ContainersTableHead({ table }: { table: TableType<ContainerRecord> }) {
return ( return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2"> <TableHeader className="sticky top-0 z-50 w-full border-b-2">
<div className="absolute -top-2 left-0 w-full h-4 bg-table-header z-50"></div>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {

View File

@@ -1,6 +1,6 @@
import { Trans, useLingui } from "@lingui/react/macro" import { Trans, useLingui } from "@lingui/react/macro"
import { LanguagesIcon } from "lucide-react" import { LanguagesIcon } from "lucide-react"
import { Button } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { dynamicActivate } from "@/lib/i18n" import { dynamicActivate } from "@/lib/i18n"
import languages from "@/lib/languages" import languages from "@/lib/languages"
@@ -14,17 +14,14 @@ export function LangToggle() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant={"ghost"} size="icon" className="hidden sm:flex"> <DropdownMenuTrigger className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
<LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" /> <LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" />
<span className="sr-only">{LangTrans}</span> <span className="sr-only">{LangTrans}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{LangTrans}</TooltipContent> <TooltipContent>{LangTrans}</TooltipContent>
</Tooltip>
</DropdownMenuTrigger> </DropdownMenuTrigger>
</TooltipTrigger>
<DropdownMenuContent className="grid grid-cols-3"> <DropdownMenuContent className="grid grid-cols-3">
{languages.map(([lang, label, e]) => ( {languages.map(([lang, label, e]) => (
<DropdownMenuItem <DropdownMenuItem
@@ -39,6 +36,7 @@ export function LangToggle() {
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</Tooltip>
</DropdownMenu> </DropdownMenu>
) )
} }

View File

@@ -10,7 +10,7 @@ export function ModeToggle() {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
<Button <Button
variant={"ghost"} variant={"ghost"}
size="icon" size="icon"

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { import {
@@ -6,6 +7,8 @@ import {
HardDriveIcon, HardDriveIcon,
LogOutIcon, LogOutIcon,
LogsIcon, LogsIcon,
MenuIcon,
PlusIcon,
SearchIcon, SearchIcon,
ServerIcon, ServerIcon,
SettingsIcon, SettingsIcon,
@@ -21,15 +24,18 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { isAdmin, isReadOnlyUser, logOut, pb } from "@/lib/api" import { isAdmin, isReadOnlyUser, logOut, pb } from "@/lib/api"
import { cn, runOnce } from "@/lib/utils" import { cn, runOnce } from "@/lib/utils"
import { AddSystemButton } from "./add-system" import { AddSystemDialog } from "./add-system"
import { LangToggle } from "./lang-toggle" import { LangToggle } from "./lang-toggle"
import { Logo } from "./logo" import { Logo } from "./logo"
import { ModeToggle } from "./mode-toggle" import { ModeToggle } from "./mode-toggle"
import { $router, basePath, Link, prependBasePath } from "./router" import { $router, basePath, Link, navigate, prependBasePath } from "./router"
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
const CommandPalette = lazy(() => import("./command-palette")) const CommandPalette = lazy(() => import("./command-palette"))
@@ -37,20 +43,117 @@ const CommandPalette = lazy(() => import("./command-palette"))
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0 const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar() { export default function Navbar() {
const [addSystemDialogOpen, setAddSystemDialogOpen] = useState(false)
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
const AdminLinks = AdminDropdownGroup()
const systemTranslation = t`System`
return ( return (
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4"> <div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
<Suspense>
<CommandPalette open={commandPaletteOpen} setOpen={setCommandPaletteOpen} />
</Suspense>
<AddSystemDialog open={addSystemDialogOpen} setOpen={setAddSystemDialogOpen} />
<Link <Link
href={basePath} href={basePath}
aria-label="Home" aria-label="Home"
className="p-2 ps-0 me-3 group" className="p-2 ps-0 me-3 group"
onMouseEnter={runOnce(() => import("@/components/routes/home"))} onMouseEnter={runOnce(() => import("@/components/routes/home"))}
> >
<Logo className="h-[1.1rem] md:h-5 fill-foreground" /> <Logo className="h-[1.2rem] md:h-5 fill-foreground" />
</Link> </Link>
<SearchButton /> <Button
variant="outline"
className="hidden md:block text-sm text-muted-foreground px-4"
onClick={() => setCommandPaletteOpen(true)}
>
<span className="flex items-center">
<SearchIcon className="me-1.5 h-4 w-4" />
<Trans>Search</Trans>
<span className="flex items-center ms-3.5">
<Kbd>{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd>
</span>
</span>
</Button>
{/* mobile menu */}
<div className="ms-auto flex items-center text-xl md:hidden">
<ModeToggle />
<Button variant="ghost" size="icon" onClick={() => setCommandPaletteOpen(true)}>
<SearchIcon className="h-[1.2rem] w-[1.2rem]" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
onMouseEnter={() => import("@/components/routes/settings/general")}
className="ms-3"
aria-label="Open Menu"
>
<MenuIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel className="max-w-40 truncate">{pb.authStore.record?.email}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => navigate(getPagePath($router, "containers"))}
className="flex items-center"
>
<ContainerIcon className="h-4 w-4 me-2.5" strokeWidth={1.5} />
<Trans>All Containers</Trans>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate(getPagePath($router, "smart"))} className="flex items-center">
<HardDriveIcon className="h-4 w-4 me-2.5" strokeWidth={1.5} />
<span>S.M.A.R.T.</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigate(getPagePath($router, "settings", { name: "general" }))}
className="flex items-center"
>
<SettingsIcon className="h-4 w-4 me-2.5" />
<Trans>Settings</Trans>
</DropdownMenuItem>
{isAdmin() && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserIcon className="h-4 w-4 me-2.5" />
<Trans>Admin</Trans>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>{AdminLinks}</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{!isReadOnlyUser() && (
<DropdownMenuItem
className="flex items-center"
onSelect={() => {
setAddSystemDialogOpen(true)
}}
>
<PlusIcon className="h-4 w-4 me-2.5" />
<Trans>Add {{ foo: systemTranslation }}</Trans>
</DropdownMenuItem>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onSelect={logOut} className="flex items-center">
<LogOutIcon className="h-4 w-4 me-2.5" />
<Trans>Log Out</Trans>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* desktop nav */}
{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */} {/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}> <div
className="hidden md:flex items-center ms-auto"
onMouseEnter={() => import("@/components/routes/settings/general")}
>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link <Link
@@ -102,9 +205,40 @@ export default function Navbar() {
<DropdownMenuContent align={isReadOnlyUser() ? "end" : "center"} className="min-w-44"> <DropdownMenuContent align={isReadOnlyUser() ? "end" : "center"} className="min-w-44">
<DropdownMenuLabel>{pb.authStore.record?.email}</DropdownMenuLabel> <DropdownMenuLabel>{pb.authStore.record?.email}</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup>
{isAdmin() && ( {isAdmin() && (
<> <>
{AdminLinks}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onSelect={logOut}>
<LogOutIcon className="me-2.5 h-4 w-4" />
<span>
<Trans>Log Out</Trans>
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{!isReadOnlyUser() && (
<Button variant="outline" className="flex gap-1 ms-2" onClick={() => setAddSystemDialogOpen(true)}>
<PlusIcon className="h-4 w-4 -ms-1" />
<Trans>Add {{ foo: systemTranslation }}</Trans>
</Button>
)}
</div>
</div>
)
}
const Kbd = ({ children }: { children: React.ReactNode }) => (
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{children}
</kbd>
)
function AdminDropdownGroup() {
return (
<DropdownMenuGroup>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<a href={prependBasePath("/_/")} target="_blank"> <a href={prependBasePath("/_/")} target="_blank">
<UsersIcon className="me-2.5 h-4 w-4" /> <UsersIcon className="me-2.5 h-4 w-4" />
@@ -137,52 +271,6 @@ export default function Navbar() {
</span> </span>
</a> </a>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuItem onSelect={logOut}>
<LogOutIcon className="me-2.5 h-4 w-4" />
<span>
<Trans>Log Out</Trans>
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<AddSystemButton className="ms-2" />
</div>
</div>
)
}
const Kbd = ({ children }: { children: React.ReactNode }) => (
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{children}
</kbd>
)
function SearchButton() {
const [open, setOpen] = useState(false)
return (
<>
<Button
variant="outline"
className="hidden md:block text-sm text-muted-foreground px-4"
onClick={() => setOpen(true)}
>
<span className="flex items-center">
<SearchIcon className="me-1.5 h-4 w-4" />
<Trans>Search</Trans>
<span className="flex items-center ms-3.5">
<Kbd>{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd>
</span>
</span>
</Button>
<Suspense>
<CommandPalette open={open} setOpen={setOpen} />
</Suspense>
</>
) )
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
import { t } from "@lingui/core/macro"
import { Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { XIcon } from "lucide-react"
import React, { type JSX, memo, useCallback, useEffect, useState } from "react"
import { $containerFilter, $maxValues } from "@/lib/stores"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import { cn } from "@/lib/utils"
import Spinner from "../../spinner"
import { Button } from "../../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../../ui/card"
import { ChartAverage, ChartMax } from "../../ui/icons"
import { Input } from "../../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select"
export function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
const storeValue = useStore(store)
const [inputValue, setInputValue] = useState(storeValue)
const { t } = useLingui()
useEffect(() => {
setInputValue(storeValue)
}, [storeValue])
useEffect(() => {
if (inputValue === storeValue) {
return
}
const handle = window.setTimeout(() => store.set(inputValue), 80)
return () => clearTimeout(handle)
}, [inputValue, storeValue, store])
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInputValue(value)
}, [])
const handleClear = useCallback(() => {
setInputValue("")
store.set("")
}, [store])
return (
<>
<Input
placeholder={t`Filter...`}
className="ps-4 pe-8 w-full sm:w-44"
onChange={handleChange}
value={inputValue}
/>
{inputValue && (
<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={handleClear}
>
<XIcon className="h-4 w-4" />
</Button>
)}
</>
)
}
export 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 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>
<SelectContent>
<SelectItem key="avg" value="avg">
<Trans>Average</Trans>
</SelectItem>
<SelectItem key="max" value="max">
<Trans comment="Chart select field. Please try to keep this short.">Max 1 min</Trans>
</SelectItem>
</SelectContent>
</Select>
)
})
export function ChartCard({
title,
description,
children,
grid,
empty,
cornerEl,
legend,
className,
}: {
title: string
description: string
children: React.ReactNode
grid?: boolean
empty?: boolean
cornerEl?: JSX.Element | null
legend?: boolean
className?: string
}) {
const { isIntersecting, ref } = useIntersectionObserver()
return (
<Card
className={cn(
"px-3 py-5 sm:py-6 sm:px-6 odd:last-of-type:col-span-full min-h-full",
{ "col-span-full": !grid },
className
)}
ref={ref}
>
<CardHeader className="gap-1.5 relative p-0 mb-3 sm:mb-4">
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
{cornerEl && <div className="grid sm:justify-end sm:absolute sm:top-0 sm:end-0 my-1 sm:my-0">{cornerEl}</div>}
</CardHeader>
<div className={cn("ps-0 -me-1 -ms-3.5 relative group", legend ? "h-54 md:h-56" : "h-48 md:h-52")}>
{
<Spinner
msg={empty ? t`Waiting for enough records to display` : undefined}
className="group-has-[.opacity-100]:invisible duration-100"
/>
}
{isIntersecting && children}
</div>
</Card>
)
}

View File

@@ -0,0 +1,116 @@
import { timeTicks } from "d3-time"
import { getPbTimestamp, pb } from "@/lib/api"
import { chartTimeData } from "@/lib/utils"
import type { ChartData, ChartTimes, ContainerStatsRecord, SystemStatsRecord } from "@/types"
type ChartTimeData = {
time: number
data: {
ticks: number[]
domain: number[]
}
chartTime: ChartTimes
}
export const cache = new Map<
string,
ChartTimeData | SystemStatsRecord[] | ContainerStatsRecord[] | ChartData["containerData"]
>()
// create ticks and domain for charts
export function getTimeData(chartTime: ChartTimes, lastCreated: number) {
const cached = cache.get("td") as ChartTimeData | undefined
if (cached && cached.chartTime === chartTime) {
if (!lastCreated || cached.time >= lastCreated) {
return cached.data
}
}
// const buffer = chartTime === "1m" ? 400 : 20_000
const now = new Date(Date.now())
const startTime = chartTimeData[chartTime].getOffset(now)
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
const data = {
ticks,
domain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()],
}
cache.set("td", { time: now.getTime(), data, chartTime })
return data
}
/** Append new records onto prev with gap detection. Converts string `created` values to ms timestamps in place.
* Pass `maxLen` to cap the result length in one copy instead of slicing again after the call. */
export function appendData<T extends { created: string | number | null }>(
prev: T[],
newRecords: T[],
expectedInterval: number,
maxLen?: number
): T[] {
if (!newRecords.length) return prev
// Pre-trim prev so the single slice() below is the only copy we make
const trimmed = maxLen && prev.length >= maxLen ? prev.slice(-(maxLen - newRecords.length)) : prev
const result = trimmed.slice()
let prevTime = (trimmed.at(-1)?.created as number) ?? 0
for (const record of newRecords) {
if (record.created !== null) {
if (typeof record.created === "string") {
record.created = new Date(record.created).getTime()
}
if (prevTime && (record.created as number) - prevTime > expectedInterval * 1.5) {
result.push({ created: null, ...("stats" in record ? { stats: null } : {}) } as T)
}
prevTime = record.created as number
}
result.push(record)
}
return result
}
export async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
collection: string,
systemId: string,
chartTime: ChartTimes
): Promise<T[]> {
const cachedStats = cache.get(`${systemId}_${chartTime}_${collection}`) as T[] | undefined
const lastCached = cachedStats?.at(-1)?.created as number
return await pb.collection<T>(collection).getFullList({
filter: pb.filter("system={:id} && created > {:created} && type={:type}", {
id: systemId,
created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),
type: chartTimeData[chartTime].type,
}),
fields: "created,stats",
sort: "created",
})
}
export function makeContainerData(containers: ContainerStatsRecord[]): ChartData["containerData"] {
const result = [] as ChartData["containerData"]
for (const { created, stats } of containers) {
if (!created) {
result.push({ created: null } as ChartData["containerData"][0])
continue
}
result.push(makeContainerPoint(new Date(created).getTime(), stats))
}
return result
}
/** Transform a single realtime container stats message into a ChartDataContainer point. */
export function makeContainerPoint(
created: number,
stats: ContainerStatsRecord["stats"]
): ChartData["containerData"][0] {
const point: ChartData["containerData"][0] = { created } as ChartData["containerData"][0]
for (const container of stats) {
;(point as Record<string, unknown>)[container.n] = container
}
return point
}
export function dockerOrPodman(str: string, isPodman: boolean): string {
if (isPodman) {
return str.replace("docker", "podman").replace("Docker", "Podman")
}
return str
}

View File

@@ -0,0 +1,99 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { useContainerDataPoints } from "@/components/charts/hooks"
import { decimalString, toFixedFloat } from "@/lib/utils"
import type { ChartConfig } from "@/components/ui/chart"
import type { ChartData } from "@/types"
import { pinnedAxisDomain } from "@/components/ui/chart"
import CpuCoresSheet from "../cpu-sheet"
import { ChartCard, FilterBar, SelectAvgMax } from "../chart-card"
import { dockerOrPodman } from "../chart-data"
export function CpuChart({
chartData,
grid,
dataEmpty,
showMax,
isLongerChart,
maxValues,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
showMax: boolean
isLongerChart: boolean
maxValues: boolean
}) {
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`CPU Usage`}
description={t`Average system-wide CPU utilization`}
cornerEl={
<div className="flex gap-2">
{maxValSelect}
<CpuCoresSheet chartData={chartData} dataEmpty={dataEmpty} grid={grid} maxValues={maxValues} />
</div>
}
>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
dataPoints={[
{
label: t`CPU Usage`,
dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),
color: 1,
opacity: 0.4,
},
]}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
domain={pinnedAxisDomain()}
/>
</ChartCard>
)
}
export function ContainerCpuChart({
chartData,
grid,
dataEmpty,
isPodman,
cpuConfig,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
isPodman: boolean
cpuConfig: ChartConfig
}) {
const { filter, dataPoints } = useContainerDataPoints(cpuConfig, (key, data) => data[key]?.c ?? null)
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker CPU Usage`, isPodman)}
description={t`Average CPU utilization of containers`}
cornerEl={<FilterBar />}
>
<AreaChartDefault
chartData={chartData}
customData={chartData.containerData}
dataPoints={dataPoints}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
domain={pinnedAxisDomain()}
showTotal={true}
reverseStackOrder={true}
filter={filter}
truncate={true}
itemSorter={(a, b) => b.value - a.value}
/>
</ChartCard>
)
}

View File

@@ -0,0 +1,283 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { SystemStatsRecord } from "@/types"
import { ChartCard, SelectAvgMax } from "../chart-card"
import { Unit } from "@/lib/enums"
import { pinnedAxisDomain } from "@/components/ui/chart"
import DiskIoSheet from "../disk-io-sheet"
import type { SystemData } from "../use-system-data"
import { useStore } from "@nanostores/react"
import { $userSettings } from "@/lib/stores"
// Helpers for indexed dios/diosm access
const dios =
(i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.dios?.[i] ?? 0
const diosMax =
(i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.diosm?.[i] ?? 0
const extraDios =
(name: string, i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.dios?.[i] ?? 0
const extraDiosMax =
(name: string, i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.diosm?.[i] ?? 0
export const diskDataFns = {
// usage
usage: ({ stats }: SystemStatsRecord) => stats?.du ?? 0,
extraUsage:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.du ?? 0,
// throughput
read: ({ stats }: SystemStatsRecord) => stats?.dio?.[0] ?? (stats?.dr ?? 0) * 1024 * 1024,
readMax: ({ stats }: SystemStatsRecord) => stats?.diom?.[0] ?? (stats?.drm ?? 0) * 1024 * 1024,
write: ({ stats }: SystemStatsRecord) => stats?.dio?.[1] ?? (stats?.dw ?? 0) * 1024 * 1024,
writeMax: ({ stats }: SystemStatsRecord) => stats?.diom?.[1] ?? (stats?.dwm ?? 0) * 1024 * 1024,
// extra fs throughput
extraRead:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.rb ?? (stats?.efs?.[name]?.r ?? 0) * 1024 * 1024,
extraReadMax:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.rbm ?? (stats?.efs?.[name]?.rm ?? 0) * 1024 * 1024,
extraWrite:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.wb ?? (stats?.efs?.[name]?.w ?? 0) * 1024 * 1024,
extraWriteMax:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.wbm ?? (stats?.efs?.[name]?.wm ?? 0) * 1024 * 1024,
// read/write time
readTime: dios(0),
readTimeMax: diosMax(0),
extraReadTime: (name: string) => extraDios(name, 0),
extraReadTimeMax: (name: string) => extraDiosMax(name, 0),
writeTime: dios(1),
writeTimeMax: diosMax(1),
extraWriteTime: (name: string) => extraDios(name, 1),
extraWriteTimeMax: (name: string) => extraDiosMax(name, 1),
// utilization (IoTime-based, 0-100%)
util: dios(2),
utilMax: diosMax(2),
extraUtil: (name: string) => extraDios(name, 2),
extraUtilMax: (name: string) => extraDiosMax(name, 2),
// r_await / w_await: average service time per read/write operation (ms)
rAwait: dios(3),
rAwaitMax: diosMax(3),
extraRAwait: (name: string) => extraDios(name, 3),
extraRAwaitMax: (name: string) => extraDiosMax(name, 3),
wAwait: dios(4),
wAwaitMax: diosMax(4),
extraWAwait: (name: string) => extraDios(name, 4),
extraWAwaitMax: (name: string) => extraDiosMax(name, 4),
// average queue depth: stored as queue_depth * 100 in Go, divided here
weightedIO: ({ stats }: SystemStatsRecord) => (stats?.dios?.[5] ?? 0) / 100,
weightedIOMax: ({ stats }: SystemStatsRecord) => (stats?.diosm?.[5] ?? 0) / 100,
extraWeightedIO:
(name: string) =>
({ stats }: SystemStatsRecord) =>
(stats?.efs?.[name]?.dios?.[5] ?? 0) / 100,
extraWeightedIOMax:
(name: string) =>
({ stats }: SystemStatsRecord) =>
(stats?.efs?.[name]?.diosm?.[5] ?? 0) / 100,
}
export function RootDiskCharts({ systemData }: { systemData: SystemData }) {
return (
<>
<DiskUsageChart systemData={systemData} />
<DiskIOChart systemData={systemData} />
</>
)
}
export function DiskUsageChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
const { chartData, grid, dataEmpty } = systemData
let diskSize = chartData.systemStats?.at(-1)?.stats.d ?? NaN
if (extraFsName) {
diskSize = chartData.systemStats?.at(-1)?.stats.efs?.[extraFsName]?.d ?? NaN
}
// round to nearest GB
if (diskSize >= 100) {
diskSize = Math.round(diskSize)
}
const title = extraFsName ? `${extraFsName} ${t`Usage`}` : t`Disk Usage`
const description = extraFsName ? t`Disk usage of ${extraFsName}` : t`Usage of root partition`
return (
<ChartCard empty={dataEmpty} grid={grid} title={title} description={description}>
<AreaChartDefault
chartData={chartData}
domain={[0, diskSize]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${decimalString(convertedValue)} ${unit}`
}}
dataPoints={[
{
label: t`Disk Usage`,
color: 4,
opacity: 0.4,
dataKey: extraFsName ? diskDataFns.extraUsage(extraFsName) : diskDataFns.usage,
},
]}
></AreaChartDefault>
</ChartCard>
)
}
export function DiskIOChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
const { chartData, grid, dataEmpty, showMax, isLongerChart, maxValues } = systemData
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const userSettings = useStore($userSettings)
if (!chartData.systemStats?.length) {
return null
}
const title = extraFsName ? `${extraFsName} I/O` : t`Disk I/O`
const description = extraFsName ? t`Throughput of ${extraFsName}` : t`Throughput of root filesystem`
const hasMoreIOMetrics = chartData.systemStats?.some((record) => record.stats?.dios?.at(0))
let CornerEl = maxValSelect
if (hasMoreIOMetrics) {
CornerEl = (
<div className="flex gap-2">
{maxValSelect}
<DiskIoSheet systemData={systemData} extraFsName={extraFsName} title={title} description={description} />
</div>
)
}
let readFn = showMax ? diskDataFns.readMax : diskDataFns.read
let writeFn = showMax ? diskDataFns.writeMax : diskDataFns.write
if (extraFsName) {
readFn = showMax ? diskDataFns.extraReadMax(extraFsName) : diskDataFns.extraRead(extraFsName)
writeFn = showMax ? diskDataFns.extraWriteMax(extraFsName) : diskDataFns.extraWrite(extraFsName)
}
return (
<ChartCard empty={dataEmpty} grid={grid} title={title} description={description} cornerEl={CornerEl}>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
// domain={pinnedAxisDomain(true)}
showTotal={true}
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: writeFn,
color: 3,
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: readFn,
color: 1,
opacity: 0.3,
},
]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
)
}
export function DiskUtilizationChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
const { chartData, grid, dataEmpty, showMax, isLongerChart, maxValues } = systemData
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
if (!chartData.systemStats?.length) {
return null
}
let utilFn = showMax ? diskDataFns.utilMax : diskDataFns.util
if (extraFsName) {
utilFn = showMax ? diskDataFns.extraUtilMax(extraFsName) : diskDataFns.extraUtil(extraFsName)
}
return (
<ChartCard
cornerEl={maxValSelect}
empty={dataEmpty}
grid={grid}
title={t({
message: `I/O Utilization`,
context: "Percent of time the disk is busy with I/O",
})}
description={t`Percent of time the disk is busy with I/O`}
// legend={true}
className="min-h-auto"
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
maxToggled={showMax}
chartProps={{ syncId: "io" }}
dataPoints={[
{
label: t({ message: "Utilization", context: "Disk I/O utilization" }),
dataKey: utilFn,
color: 1,
opacity: 0.4,
},
]}
/>
</ChartCard>
)
}
export function ExtraFsCharts({ systemData }: { systemData: SystemData }) {
const { systemStats } = systemData.chartData
const extraFs = systemStats?.at(-1)?.stats.efs
if (!extraFs || Object.keys(extraFs).length === 0) {
return null
}
return (
<div className="grid xl:grid-cols-2 gap-4">
{Object.keys(extraFs).map((extraFsName) => {
let diskSize = systemStats.at(-1)?.stats.efs?.[extraFsName].d ?? NaN
// round to nearest GB
if (diskSize >= 100) {
diskSize = Math.round(diskSize)
}
return (
<div key={extraFsName} className="contents">
<DiskUsageChart systemData={systemData} extraFsName={extraFsName} />
<DiskIOChart systemData={systemData} extraFsName={extraFsName} />
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,232 @@
import { t } from "@lingui/core/macro"
import { useRef, useMemo } from "react"
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
import LineChartDefault from "@/components/charts/line-chart"
import { Unit } from "@/lib/enums"
import { cn, decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData, GPUData, SystemStatsRecord } from "@/types"
import { ChartCard } from "../chart-card"
/** GPU power draw chart for the main grid */
export function GpuPowerChart({
chartData,
grid,
dataEmpty,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
}) {
const packageKey = " package"
const statsRef = useRef(chartData.systemStats)
statsRef.current = chartData.systemStats
// Derive GPU power config key (cheap per render)
let gpuPowerKey = ""
for (let i = chartData.systemStats.length - 1; i >= 0; i--) {
const gpus = chartData.systemStats[i].stats?.g
if (gpus) {
const parts: string[] = []
for (const id in gpus) {
const gpu = gpus[id] as GPUData
if (gpu.p !== undefined) parts.push(`${id}:${gpu.n}`)
if (gpu.pp !== undefined) parts.push(`${id}:${gpu.n}${packageKey}`)
}
gpuPowerKey = parts.sort().join("\0")
break
}
}
const dataPoints = useMemo((): DataPoint[] => {
if (!gpuPowerKey) return []
const totals = new Map<string, { label: string; gpuId: string; isPackage: boolean; total: number }>()
for (const record of statsRef.current) {
const gpus = record.stats?.g
if (!gpus) continue
for (const id in gpus) {
const gpu = gpus[id] as GPUData
const key = gpu.n
const existing = totals.get(key)
if (existing) {
existing.total += gpu.p ?? 0
} else {
totals.set(key, { label: gpu.n, gpuId: id, isPackage: false, total: gpu.p ?? 0 })
}
if (gpu.pp !== undefined) {
const pkgKey = `${gpu.n}${packageKey}`
const existingPkg = totals.get(pkgKey)
if (existingPkg) {
existingPkg.total += gpu.pp
} else {
totals.set(pkgKey, { label: pkgKey, gpuId: id, isPackage: true, total: gpu.pp })
}
}
}
}
const sorted = Array.from(totals.values()).sort((a, b) => b.total - a.total)
return sorted.map(
(entry, i): DataPoint => ({
label: entry.label,
dataKey: (data: SystemStatsRecord) => {
const gpu = data.stats?.g?.[entry.gpuId]
return entry.isPackage ? (gpu?.pp ?? 0) : (gpu?.p ?? 0)
},
color: `hsl(${226 + (((i * 360) / sorted.length) % 360)}, 65%, 52%)`,
opacity: 1,
})
)
}, [gpuPowerKey])
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`GPU Power Draw`}
description={t`Average power consumption of GPUs`}
>
<LineChartDefault
legend={dataPoints.length > 1}
chartData={chartData}
dataPoints={dataPoints}
itemSorter={(a: { value: number }, b: { value: number }) => b.value - a.value}
tickFormatter={(val) => `${toFixedFloat(val, 2)}W`}
contentFormatter={({ value }) => `${decimalString(value)}W`}
/>
</ChartCard>
)
}
/** GPU detail grid (engines + per-GPU usage/VRAM) — rendered outside the main 2-col grid */
export function GpuDetailCharts({
chartData,
grid,
dataEmpty,
lastGpus,
hasGpuEnginesData,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
lastGpus: Record<string, GPUData>
hasGpuEnginesData: boolean
}) {
return (
<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(lastGpus).map((id) => {
const gpu = lastGpus[id] as GPUData
return (
<div key={id} className="contents">
<ChartCard
className={cn(grid && "!col-span-1")}
empty={dataEmpty}
grid={grid}
title={`${gpu.n} ${t`Usage`}`}
description={t`Average utilization of ${gpu.n}`}
>
<AreaChartDefault
chartData={chartData}
dataPoints={[
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
color: 1,
opacity: 0.35,
},
]}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
/>
</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>
)
}
function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
// Derive stable engine config key (cheap per render)
let enginesKey = ""
for (let i = chartData.systemStats.length - 1; i >= 0; i--) {
const gpus = chartData.systemStats[i].stats?.g
if (!gpus) continue
for (const id in gpus) {
if (gpus[id].e) {
enginesKey = id + "\0" + Object.keys(gpus[id].e).sort().join("\0")
break
}
}
if (enginesKey) break
}
const { gpuId, dataPoints } = useMemo((): { gpuId: string | null; dataPoints: DataPoint[] } => {
if (!enginesKey) return { gpuId: null, dataPoints: [] }
const parts = enginesKey.split("\0")
const gId = parts[0]
const engineNames = parts.slice(1)
return {
gpuId: gId,
dataPoints: engineNames.map((engine, i) => ({
label: engine,
dataKey: ({ stats }: SystemStatsRecord) => stats?.g?.[gId]?.e?.[engine] ?? 0,
color: `hsl(${140 + (((i * 360) / engineNames.length) % 360)}, 65%, 52%)`,
opacity: 0.35,
})),
}
}, [enginesKey])
if (!gpuId) {
return null
}
return (
<LineChartDefault
legend={true}
chartData={chartData}
dataPoints={dataPoints}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
/>
)
}

View File

@@ -0,0 +1,55 @@
import { t } from "@lingui/core/macro"
import type { ChartData } from "@/types"
import { ChartCard } from "../chart-card"
import LineChartDefault from "@/components/charts/line-chart"
import { decimalString, toFixedFloat } from "@/lib/utils"
export function LoadAverageChart({
chartData,
grid,
dataEmpty,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
}) {
const { major, minor } = chartData.agentVersion
if (major === 0 && minor <= 12) {
return null
}
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Load Average`}
description={t`System load averages over time`}
legend={true}
>
<LineChartDefault
chartData={chartData}
contentFormatter={(item) => decimalString(item.value)}
tickFormatter={(value) => {
return String(toFixedFloat(value, 2))
}}
legend={true}
dataPoints={[
{
label: t({ message: `1 min`, comment: "Load average" }),
color: "hsl(271, 81%, 60%)", // Purple
dataKey: ({ stats }) => stats?.la?.[0],
},
{
label: t({ message: `5 min`, comment: "Load average" }),
color: "hsl(217, 91%, 60%)", // Blue
dataKey: ({ stats }) => stats?.la?.[1],
},
{
label: t({ message: `15 min`, comment: "Load average" }),
color: "hsl(25, 95%, 53%)", // Orange
dataKey: ({ stats }) => stats?.la?.[2],
},
]}
></LineChartDefault>
</ChartCard>
)
}

View File

@@ -0,0 +1,170 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { useContainerDataPoints } from "@/components/charts/hooks"
import { Unit } from "@/lib/enums"
import type { ChartConfig } from "@/components/ui/chart"
import type { ChartData, SystemStatsRecord } from "@/types"
import { ChartCard, FilterBar, SelectAvgMax } from "../chart-card"
import { dockerOrPodman } from "../chart-data"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import { pinnedAxisDomain } from "@/components/ui/chart"
export function MemoryChart({
chartData,
grid,
dataEmpty,
showMax,
isLongerChart,
maxValues,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
showMax: boolean
isLongerChart: boolean
maxValues: boolean
}) {
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const totalMem = toFixedFloat(chartData.systemStats.at(-1)?.stats.m ?? 0, 1)
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Memory Usage`}
description={t`Precise utilization at the recorded time`}
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
domain={[0, totalMem]}
itemSorter={(a, b) => a.order - b.order}
maxToggled={showMax}
showTotal={true}
tickFormatter={(value) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${toFixedFloat(convertedValue, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
dataPoints={[
{
label: t`Used`,
dataKey: ({ stats }) => (showMax ? stats?.mm : stats?.mu),
color: 2,
opacity: 0.4,
stackId: "1",
order: 3,
},
{
label: "ZFS ARC",
dataKey: ({ stats }) => (showMax ? null : stats?.mz),
color: "hsla(175 60% 45% / 0.8)",
opacity: 0.5,
order: 2,
},
{
label: t`Cache / Buffers`,
dataKey: ({ stats }) => (showMax ? null : stats?.mb),
color: "hsla(160 60% 45% / 0.5)",
opacity: 0.4,
stackId: "1",
order: 1,
},
]}
/>
</ChartCard>
)
}
export function ContainerMemoryChart({
chartData,
grid,
dataEmpty,
isPodman,
memoryConfig,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
isPodman: boolean
memoryConfig: ChartConfig
}) {
const { filter, dataPoints } = useContainerDataPoints(memoryConfig, (key, data) => data[key]?.m ?? null)
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Memory Usage`, isPodman)}
description={dockerOrPodman(t`Memory usage of docker containers`, isPodman)}
cornerEl={<FilterBar />}
>
<AreaChartDefault
chartData={chartData}
customData={chartData.containerData}
dataPoints={dataPoints}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
return `${toFixedFloat(value, val >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={(item) => {
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
return `${decimalString(value)} ${unit}`
}}
domain={pinnedAxisDomain()}
showTotal={true}
reverseStackOrder={true}
filter={filter}
truncate={true}
itemSorter={(a, b) => b.value - a.value}
/>
</ChartCard>
)
}
export function SwapChart({
chartData,
grid,
dataEmpty,
systemStats,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
systemStats: SystemStatsRecord[]
}) {
// const userSettings = useStore($userSettings)
const hasSwapData = (systemStats.at(-1)?.stats.su ?? 0) > 0
if (!hasSwapData) {
return null
}
return (
<ChartCard empty={dataEmpty} grid={grid} title={t`Swap Usage`} description={t`Swap space used by the system`}>
<AreaChartDefault
chartData={chartData}
domain={[0, () => toFixedFloat(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
contentFormatter={({ value }) => {
// mem values are supplied as GB
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
tickFormatter={(value) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${toFixedFloat(convertedValue, value >= 10 ? 0 : 1)} ${unit}`
}}
dataPoints={[
{
label: t`Used`,
dataKey: ({ stats }) => stats?.su,
color: 2,
opacity: 0.4,
},
]}
></AreaChartDefault>
</ChartCard>
)
}

View File

@@ -0,0 +1,183 @@
import { useMemo } from "react"
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { useContainerDataPoints } from "@/components/charts/hooks"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartConfig } from "@/components/ui/chart"
import { pinnedAxisDomain } from "@/components/ui/chart"
import type { ChartData, SystemStatsRecord } from "@/types"
import { Separator } from "@/components/ui/separator"
import NetworkSheet from "../network-sheet"
import { ChartCard, FilterBar, SelectAvgMax } from "../chart-card"
import { dockerOrPodman } from "../chart-data"
export function BandwidthChart({
chartData,
grid,
dataEmpty,
showMax,
isLongerChart,
maxValues,
systemStats,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
showMax: boolean
isLongerChart: boolean
maxValues: boolean
systemStats: SystemStatsRecord[]
}) {
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const userSettings = $userSettings.get()
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Bandwidth`}
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
chartData={chartData}
maxToggled={showMax}
dataPoints={[
{
label: t`Sent`,
dataKey(data: SystemStatsRecord) {
if (showMax) {
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
}
return data?.stats?.b?.[0] ?? (data?.stats?.ns ?? 0) * 1024 * 1024
},
color: 5,
opacity: 0.2,
},
{
label: t`Received`,
dataKey(data: SystemStatsRecord) {
if (showMax) {
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
}
return data?.stats?.b?.[1] ?? (data?.stats?.nr ?? 0) * 1024 * 1024
},
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}`
}}
contentFormatter={(data) => {
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`
}}
showTotal={true}
/>
</ChartCard>
)
}
export function ContainerNetworkChart({
chartData,
grid,
dataEmpty,
isPodman,
networkConfig,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
isPodman: boolean
networkConfig: ChartConfig
}) {
const userSettings = $userSettings.get()
const { filter, dataPoints, filteredKeys } = useContainerDataPoints(networkConfig, (key, data) => {
const payload = data[key]
if (!payload) return null
const sent = payload?.b?.[0] ?? (payload?.ns ?? 0) * 1024 * 1024
const recv = payload?.b?.[1] ?? (payload?.nr ?? 0) * 1024 * 1024
return sent + recv
})
const contentFormatter = useMemo(() => {
const getRxTxBytes = (record?: { b?: [number, number]; ns?: number; nr?: number }) => {
if (record?.b?.length && record.b.length >= 2) {
return [Number(record.b[0]) || 0, Number(record.b[1]) || 0]
}
return [(record?.ns ?? 0) * 1024 * 1024, (record?.nr ?? 0) * 1024 * 1024]
}
const formatRxTx = (recv: number, sent: number) => {
const { value: receivedValue, unit: receivedUnit } = formatBytes(recv, true, userSettings.unitNet, false)
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, false)
return (
<span className="flex">
{decimalString(receivedValue)} {receivedUnit}
<span className="opacity-70 ms-0.5"> rx </span>
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
{decimalString(sentValue)} {sentUnit}
<span className="opacity-70 ms-0.5"> tx</span>
</span>
)
}
// biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item
return (item: any, key: string) => {
try {
if (key === "__total__") {
let totalSent = 0
let totalRecv = 0
const payloadData = item?.payload && typeof item.payload === "object" ? item.payload : {}
for (const [containerKey, value] of Object.entries(payloadData)) {
if (!value || typeof value !== "object") continue
if (filteredKeys.has(containerKey)) continue
const [sent, recv] = getRxTxBytes(value as { b?: [number, number]; ns?: number; nr?: number })
totalSent += sent
totalRecv += recv
}
return formatRxTx(totalRecv, totalSent)
}
const [sent, recv] = getRxTxBytes(item?.payload?.[key])
return formatRxTx(recv, sent)
} catch {
return null
}
}
}, [filteredKeys, userSettings.unitNet])
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Network I/O`, isPodman)}
description={dockerOrPodman(t`Network traffic of docker containers`, isPodman)}
cornerEl={<FilterBar />}
>
<AreaChartDefault
chartData={chartData}
customData={chartData.containerData}
dataPoints={dataPoints}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={contentFormatter}
domain={pinnedAxisDomain()}
showTotal={true}
reverseStackOrder={true}
filter={filter}
truncate={true}
itemSorter={(a, b) => b.value - a.value}
/>
</ChartCard>
)
}

View File

@@ -0,0 +1,209 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { batteryStateTranslations } from "@/lib/i18n"
import { $temperatureFilter, $userSettings } from "@/lib/stores"
import { cn, decimalString, formatTemperature, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { ChartCard, FilterBar } from "../chart-card"
import LineChartDefault from "@/components/charts/line-chart"
import { useStore } from "@nanostores/react"
import { useRef, useMemo, useState, useEffect } from "react"
export function BatteryChart({
chartData,
grid,
dataEmpty,
maxValues,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
maxValues: boolean
}) {
const showBatteryChart = chartData.systemStats.at(-1)?.stats.bat
if (!showBatteryChart) {
return null
}
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Battery`}
description={`${t({
message: "Current state",
comment: "Context: Battery state",
})}: ${batteryStateTranslations[chartData.systemStats.at(-1)?.stats.bat?.[1] ?? 0]()}`}
>
<AreaChartDefault
chartData={chartData}
maxToggled={maxValues}
dataPoints={[
{
label: t`Charge`,
dataKey: ({ stats }) => stats?.bat?.[0],
color: 1,
opacity: 0.35,
},
]}
domain={[0, 100]}
tickFormatter={(val) => `${val}%`}
contentFormatter={({ value }) => `${value}%`}
/>
</ChartCard>
)
}
export function TemperatureChart({
chartData,
grid,
dataEmpty,
setPageBottomExtraMargin,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
setPageBottomExtraMargin?: (margin: number) => void
}) {
const showTempChart = chartData.systemStats.at(-1)?.stats.t
const filter = useStore($temperatureFilter)
const userSettings = useStore($userSettings)
const statsRef = useRef(chartData.systemStats)
statsRef.current = chartData.systemStats
// Derive sensor names key from latest data point
let sensorNamesKey = ""
for (let i = chartData.systemStats.length - 1; i >= 0; i--) {
const t = chartData.systemStats[i].stats?.t
if (t) {
sensorNamesKey = Object.keys(t).sort().join("\0")
break
}
}
// Only recompute colors and dataKey functions when sensor names change
const { colorMap, dataKeys, sortedKeys } = useMemo(() => {
const stats = statsRef.current
const tempSums = {} as Record<string, number>
for (const data of stats) {
const t = data.stats?.t
if (!t) continue
for (const key of Object.keys(t)) {
tempSums[key] = (tempSums[key] ?? 0) + t[key]
}
}
const sorted = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
const colorMap = {} as Record<string, string>
const dataKeys = {} as Record<string, (d: SystemStatsRecord) => number | undefined>
for (let i = 0; i < sorted.length; i++) {
const key = sorted[i]
colorMap[key] = `hsl(${((i * 360) / sorted.length) % 360}, 60%, 55%)`
dataKeys[key] = (d: SystemStatsRecord) => d.stats?.t?.[key]
}
return { colorMap, dataKeys, sortedKeys: sorted }
}, [sensorNamesKey])
const dataPoints = useMemo(() => {
return sortedKeys.map((key) => {
const filterTerms = filter
? filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
: []
const filtered = filterTerms.length > 0 && !filterTerms.some((term) => key.toLowerCase().includes(term))
const strokeOpacity = filtered ? 0.1 : 1
return {
label: key,
dataKey: dataKeys[key],
color: colorMap[key],
opacity: strokeOpacity,
}
})
}, [sortedKeys, filter, dataKeys, colorMap])
// test with lots of data points
// const totalPoints = 50
// if (dataPoints.length > 0 && dataPoints.length < totalPoints) {
// let i = 0
// while (dataPoints.length < totalPoints) {
// dataPoints.push({
// label: `Test ${++i}`,
// dataKey: () => 0,
// color: "red",
// opacity: 1,
// })
// }
// }
const chartRef = useRef<HTMLDivElement>(null)
const [addMargin, setAddMargin] = useState(false)
const marginPx = (dataPoints.length - 13) * 18
useEffect(() => {
if (setPageBottomExtraMargin && dataPoints.length > 13 && chartRef.current) {
const checkPosition = () => {
if (!chartRef.current) return
const rect = chartRef.current.getBoundingClientRect()
const actualScrollHeight = addMargin
? document.documentElement.scrollHeight - marginPx
: document.documentElement.scrollHeight
const distanceToBottom = actualScrollHeight - (rect.bottom + window.scrollY)
if (distanceToBottom < 250) {
setAddMargin(true)
setPageBottomExtraMargin(marginPx)
} else {
setAddMargin(false)
setPageBottomExtraMargin(0)
}
}
checkPosition()
const timer = setTimeout(checkPosition, 500)
return () => {
clearTimeout(timer)
}
} else if (addMargin) {
setAddMargin(false)
if (setPageBottomExtraMargin) setPageBottomExtraMargin(0)
}
}, [dataPoints.length, addMargin, marginPx, setPageBottomExtraMargin])
if (!showTempChart) {
return null
}
const legend = dataPoints.length < 12
return (
<div ref={chartRef} className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Temperature`}
description={t`Temperatures of system sensors`}
cornerEl={<FilterBar store={$temperatureFilter} />}
legend={legend}
>
<LineChartDefault
chartData={chartData}
itemSorter={(a, b) => b.value - a.value}
domain={["auto", "auto"]}
legend={legend}
tickFormatter={(val) => {
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
return `${toFixedFloat(value, 2)} ${unit}`
}}
contentFormatter={(item) => {
const { value, unit } = formatTemperature(item.value, userSettings.unitTemp)
return `${decimalString(value)} ${unit}`
}}
dataPoints={dataPoints}
></LineChartDefault>
</ChartCard>
</div>
)
}

View File

@@ -1,14 +1,14 @@
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { MoreHorizontalIcon } from "lucide-react" import { MoreHorizontalIcon } from "lucide-react"
import { memo, useRef, useState } from "react" import { memo, useRef, useState } from "react"
import AreaChartDefault, { DataPoint } from "@/components/charts/area-chart" import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
import ChartTimeSelect from "@/components/charts/chart-time-select" import ChartTimeSelect from "@/components/charts/chart-time-select"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { DialogTitle } from "@/components/ui/dialog" import { DialogTitle } from "@/components/ui/dialog"
import { compareSemVer, decimalString, parseSemVer, toFixedFloat } from "@/lib/utils" import { compareSemVer, decimalString, parseSemVer, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types" import type { ChartData, SystemStatsRecord } from "@/types"
import { ChartCard } from "../system" import { ChartCard } from "./chart-card"
const minAgentVersion = parseSemVer("0.15.3") const minAgentVersion = parseSemVer("0.15.3")
@@ -42,41 +42,54 @@ export default memo(function CpuCoresSheet({
const numCores = cpus.length const numCores = cpus.length
const hasBreakdown = (latest?.cpub?.length ?? 0) > 0 const hasBreakdown = (latest?.cpub?.length ?? 0) > 0
// make sure all individual core data points have the same y axis domain to make relative comparison easier
let highestCpuCorePct = 1
if (hasOpened.current) {
for (let i = 0; i < numCores; i++) {
for (let j = 0; j < chartData.systemStats.length; j++) {
const pct = chartData.systemStats[j].stats?.cpus?.[i] ?? 0
if (pct > highestCpuCorePct) {
highestCpuCorePct = pct
}
}
}
}
const breakdownDataPoints = [ const breakdownDataPoints = [
{ {
label: "System", label: "System",
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[1], dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[1],
color: 3, color: 3,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}, },
{ {
label: "User", label: "User",
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[0], dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[0],
color: 1, color: 1,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}, },
{ {
label: "IOWait", label: "IOWait",
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[2], dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[2],
color: 4, color: 4,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}, },
{ {
label: "Steal", label: "Steal",
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[3], dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[3],
color: 5, color: 5,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}, },
{ {
label: "Idle", label: "Idle",
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[4], dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[4],
color: 2, color: 2,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}, },
{ {
label: t`Other`, label: t`Other`,
@@ -86,11 +99,10 @@ export default memo(function CpuCoresSheet({
}, },
color: `hsl(80, 65%, 52%)`, color: `hsl(80, 65%, 52%)`,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}, },
] as DataPoint[] ] as DataPoint[]
return ( return (
<Sheet open={cpuCoresOpen} onOpenChange={setCpuCoresOpen}> <Sheet open={cpuCoresOpen} onOpenChange={setCpuCoresOpen}>
<DialogTitle className="sr-only">{t`CPU Usage`}</DialogTitle> <DialogTitle className="sr-only">{t`CPU Usage`}</DialogTitle>
@@ -99,7 +111,7 @@ export default memo(function CpuCoresSheet({
title={t`View more`} title={t`View more`}
variant="outline" variant="outline"
size="icon" size="icon"
className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3" className="shrink-0 max-sm:absolute max-sm:top-0 max-sm:end-0"
> >
<MoreHorizontalIcon /> <MoreHorizontalIcon />
</Button> </Button>
@@ -151,7 +163,7 @@ export default memo(function CpuCoresSheet({
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpus?.[i] ?? 1 / (stats?.cpus?.length ?? 1), dataKey: ({ stats }: SystemStatsRecord) => stats?.cpus?.[i] ?? 1 / (stats?.cpus?.length ?? 1),
color: `hsl(${226 + (((i * 360) / Math.max(1, numCores)) % 360)}, var(--chart-saturation), var(--chart-lightness))`, color: `hsl(${226 + (((i * 360) / Math.max(1, numCores)) % 360)}, var(--chart-saturation), var(--chart-lightness))`,
opacity: 0.35, opacity: 0.35,
stackId: "a" stackId: "a",
}))} }))}
tickFormatter={(val) => `${val}%`} tickFormatter={(val) => `${val}%`}
contentFormatter={({ value }) => `${value}%`} contentFormatter={({ value }) => `${value}%`}
@@ -174,7 +186,7 @@ export default memo(function CpuCoresSheet({
<AreaChartDefault <AreaChartDefault
chartData={chartData} chartData={chartData}
maxToggled={maxValues} maxToggled={maxValues}
legend={false} domain={[0, highestCpuCorePct]}
dataPoints={[ dataPoints={[
{ {
label: t`Usage`, label: t`Usage`,

View File

@@ -0,0 +1,265 @@
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 { 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 { ChartCard, SelectAvgMax } from "@/components/routes/system/chart-card"
import type { SystemData } from "@/components/routes/system/use-system-data"
import { diskDataFns, DiskUtilizationChart } from "./charts/disk-charts"
import { pinnedAxisDomain } from "@/components/ui/chart"
export default memo(function DiskIOSheet({
systemData,
extraFsName,
title,
description,
}: {
systemData: SystemData
extraFsName?: string
title: string
description: string
}) {
const { chartData, grid, dataEmpty, showMax, maxValues, isLongerChart } = systemData
const userSettings = useStore($userSettings)
const [sheetOpen, setSheetOpen] = useState(false)
const hasOpened = useRef(false)
if (sheetOpen && !hasOpened.current) {
hasOpened.current = true
}
// throughput functions, with extra fs variants if needed
let readFn = showMax ? diskDataFns.readMax : diskDataFns.read
let writeFn = showMax ? diskDataFns.writeMax : diskDataFns.write
if (extraFsName) {
readFn = showMax ? diskDataFns.extraReadMax(extraFsName) : diskDataFns.extraRead(extraFsName)
writeFn = showMax ? diskDataFns.extraWriteMax(extraFsName) : diskDataFns.extraWrite(extraFsName)
}
// read and write time functions, with extra fs variants if needed
let readTimeFn = showMax ? diskDataFns.readTimeMax : diskDataFns.readTime
let writeTimeFn = showMax ? diskDataFns.writeTimeMax : diskDataFns.writeTime
if (extraFsName) {
readTimeFn = showMax ? diskDataFns.extraReadTimeMax(extraFsName) : diskDataFns.extraReadTime(extraFsName)
writeTimeFn = showMax ? diskDataFns.extraWriteTimeMax(extraFsName) : diskDataFns.extraWriteTime(extraFsName)
}
// I/O await functions, with extra fs variants if needed
let rAwaitFn = showMax ? diskDataFns.rAwaitMax : diskDataFns.rAwait
let wAwaitFn = showMax ? diskDataFns.wAwaitMax : diskDataFns.wAwait
if (extraFsName) {
rAwaitFn = showMax ? diskDataFns.extraRAwaitMax(extraFsName) : diskDataFns.extraRAwait(extraFsName)
wAwaitFn = showMax ? diskDataFns.extraWAwaitMax(extraFsName) : diskDataFns.extraWAwait(extraFsName)
}
// weighted I/O function, with extra fs variant if needed
let weightedIOFn = showMax ? diskDataFns.weightedIOMax : diskDataFns.weightedIO
if (extraFsName) {
weightedIOFn = showMax ? diskDataFns.extraWeightedIOMax(extraFsName) : diskDataFns.extraWeightedIO(extraFsName)
}
// check for availability of I/O metrics
let hasUtilization = false
let hasAwait = false
let hasWeightedIO = false
for (const record of chartData.systemStats ?? []) {
const dios = record.stats?.dios
if ((dios?.at(2) ?? 0) > 0) hasUtilization = true
if ((dios?.at(3) ?? 0) > 0) hasAwait = true
if ((dios?.at(5) ?? 0) > 0) hasWeightedIO = true
if (hasUtilization && hasAwait && hasWeightedIO) {
break
}
}
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const chartProps = { syncId: "io" }
const queueDepthTranslation = t({ message: "Queue Depth", context: "Disk I/O average queue depth" })
return (
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<DialogTitle className="sr-only">{title}</DialogTitle>
<SheetTrigger asChild>
<Button
title={t`View more`}
variant="outline"
size="icon"
className="shrink-0 max-sm:absolute max-sm:top-0 max-sm:end-0"
>
<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)] bg-card" agentVersion={chartData.agentVersion} />
<ChartCard
className="min-h-auto"
empty={dataEmpty}
grid={grid}
title={title}
description={description}
cornerEl={maxValSelect}
// legend={true}
>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
chartProps={chartProps}
showTotal={true}
domain={pinnedAxisDomain()}
itemSorter={(a, b) => a.order - b.order}
reverseStackOrder={true}
dataPoints={[
{
label: t`Write`,
dataKey: writeFn,
color: 3,
opacity: 0.4,
stackId: 0,
order: 0,
},
{
label: t`Read`,
dataKey: readFn,
color: 1,
opacity: 0.4,
stackId: 0,
order: 1,
},
]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
{hasUtilization && <DiskUtilizationChart systemData={systemData} extraFsName={extraFsName} />}
<ChartCard
empty={dataEmpty}
grid={grid}
title={t({ message: "I/O Time", context: "Disk I/O total time spent on read/write" })}
description={t({
message: "Total time spent on read/write (can exceed 100%)",
context: "Disk I/O",
})}
className="min-h-auto"
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
maxToggled={showMax}
chartProps={chartProps}
showTotal={true}
itemSorter={(a, b) => a.order - b.order}
reverseStackOrder={true}
dataPoints={[
{
label: t`Write`,
dataKey: writeTimeFn,
color: 3,
opacity: 0.4,
stackId: 0,
order: 0,
},
{
label: t`Read`,
dataKey: readTimeFn,
color: 1,
opacity: 0.4,
stackId: 0,
order: 1,
},
]}
/>
</ChartCard>
{hasWeightedIO && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={queueDepthTranslation}
description={t`Average number of I/O operations waiting to be serviced`}
className="min-h-auto"
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)}`}
contentFormatter={({ value }) => decimalString(value, value < 10 ? 3 : 2)}
maxToggled={showMax}
chartProps={chartProps}
dataPoints={[
{
label: queueDepthTranslation,
dataKey: weightedIOFn,
color: 1,
opacity: 0.4,
},
]}
/>
</ChartCard>
)}
{hasAwait && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t({ message: "I/O Await", context: "Disk I/O average operation time (iostat await)" })}
description={t({
message: "Average queue to completion time per operation",
context: "Disk I/O average operation time (iostat await)",
})}
className="min-h-auto"
cornerEl={maxValSelect}
// legend={true}
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)} ms`}
contentFormatter={({ value }) => `${decimalString(value)} ms`}
maxToggled={showMax}
chartProps={chartProps}
dataPoints={[
{
label: t`Write`,
dataKey: wAwaitFn,
color: 3,
opacity: 0.3,
},
{
label: t`Read`,
dataKey: rAwaitFn,
color: 1,
opacity: 0.3,
},
]}
/>
</ChartCard>
)}
</SheetContent>
)}
</Sheet>
)
})

View File

@@ -1,20 +1,28 @@
import { plural } from "@lingui/core/macro" import { plural } from "@lingui/core/macro"
import { useLingui } from "@lingui/react/macro" import { Trans, useLingui } from "@lingui/react/macro"
import { import {
AppleIcon, AppleIcon,
ChevronRightSquareIcon, ChevronRightSquareIcon,
ClockArrowUp, ClockArrowUp,
CpuIcon, CpuIcon,
GlobeIcon, GlobeIcon,
LayoutGridIcon,
MemoryStickIcon, MemoryStickIcon,
MonitorIcon, MonitorIcon,
Rows, Settings2Icon,
} from "lucide-react" } from "lucide-react"
import { useMemo } from "react" import { useMemo } from "react"
import ChartTimeSelect from "@/components/charts/chart-time-select" import ChartTimeSelect from "@/components/charts/chart-time-select"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons" import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
@@ -27,12 +35,16 @@ export default function InfoBar({
chartData, chartData,
grid, grid,
setGrid, setGrid,
displayMode,
setDisplayMode,
details, details,
}: { }: {
system: SystemRecord system: SystemRecord
chartData: ChartData chartData: ChartData
grid: boolean grid: boolean
setGrid: (grid: boolean) => void setGrid: (grid: boolean) => void
displayMode: "default" | "tabs"
setDisplayMode: (mode: "default" | "tabs") => void
details: SystemDetailsRecord | null details: SystemDetailsRecord | null
}) { }) {
const { t } = useLingui() const { t } = useLingui()
@@ -123,10 +135,10 @@ export default function InfoBar({
return ( return (
<Card> <Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5"> <div className="grid xl:flex xl:gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
<div> <div className="min-w-0">
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1> <h1 className="text-2xl sm: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="flex xl:flex-wrap items-center py-4 xl:p-0 -mt-3 xl:mt-1 gap-3 text-sm text-nowrap opacity-90 overflow-x-auto scrollbar-hide -mx-4 px-4 xl:mx-0">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="capitalize flex gap-2 items-center"> <div className="capitalize flex gap-2 items-center">
@@ -190,24 +202,53 @@ export default function InfoBar({
</div> </div>
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1"> <div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} /> <ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
<Tooltip> <DropdownMenu>
<TooltipTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
aria-label={t`Toggle grid`} aria-label={t`Settings`}
variant="outline" variant="outline"
size="icon" size="icon"
className="hidden xl:flex p-0 text-primary" className="hidden xl:flex p-0 text-primary"
onClick={() => setGrid(!grid)}
> >
{grid ? ( <Settings2Icon className="size-4 opacity-90" />
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
) : (
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
)}
</Button> </Button>
</TooltipTrigger> </DropdownMenuTrigger>
<TooltipContent>{t`Toggle grid`}</TooltipContent> <DropdownMenuContent align="end" className="min-w-44">
</Tooltip> <DropdownMenuLabel className="px-3.5">
<Trans context="Layout display options">Display</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
className="px-1 pb-1"
value={displayMode}
onValueChange={(v) => setDisplayMode(v as "default" | "tabs")}
>
<DropdownMenuRadioItem value="default" onSelect={(e) => e.preventDefault()}>
<Trans context="Default system layout option">Default</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="tabs" onSelect={(e) => e.preventDefault()}>
<Trans context="Tabs system layout option">Tabs</Trans>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel className="px-3.5">
<Trans>Chart width</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
className="px-1 pb-1"
value={grid ? "grid" : "full"}
onValueChange={(v) => setGrid(v === "grid")}
>
<DropdownMenuRadioItem value="grid" onSelect={(e) => e.preventDefault()}>
<Trans>Grid</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="full" onSelect={(e) => e.preventDefault()}>
<Trans>Full</Trans>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -0,0 +1,36 @@
import { lazy } from "react"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import { cn } from "@/lib/utils"
const ContainersTable = lazy(() => import("../../containers-table/containers-table"))
export function LazyContainersTable({ systemId }: { systemId: string }) {
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
return (
<div ref={ref} className={cn(isIntersecting && "contents")}>
{isIntersecting && <ContainersTable systemId={systemId} />}
</div>
)
}
const SmartTable = lazy(() => import("./smart-table"))
export function LazySmartTable({ systemId }: { systemId: string }) {
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
return (
<div ref={ref} className={cn(isIntersecting && "contents")}>
{isIntersecting && <SmartTable systemId={systemId} />}
</div>
)
}
const SystemdTable = lazy(() => import("../../systemd-table/systemd-table"))
export function LazySystemdTable({ systemId }: { systemId: string }) {
const { isIntersecting, ref } = useIntersectionObserver()
return (
<div ref={ref} className={cn(isIntersecting && "contents")}>
{isIntersecting && <SystemdTable systemId={systemId} />}
</div>
)
}

View File

@@ -11,7 +11,7 @@ import { DialogTitle } from "@/components/ui/dialog"
import { $userSettings } from "@/lib/stores" import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils" import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types" import type { ChartData } from "@/types"
import { ChartCard } from "../system" import { ChartCard } from "./chart-card"
export default memo(function NetworkSheet({ export default memo(function NetworkSheet({
chartData, chartData,
@@ -46,7 +46,7 @@ export default memo(function NetworkSheet({
title={t`View more`} title={t`View more`}
variant="outline" variant="outline"
size="icon" size="icon"
className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3" className="shrink-0 max-sm:absolute max-sm:top-0 max-sm:end-0"
> >
<MoreHorizontalIcon /> <MoreHorizontalIcon />
</Button> </Button>

View File

@@ -36,7 +36,7 @@ import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { pb } from "@/lib/api" import { isReadOnlyUser, pb } from "@/lib/api"
import type { SmartDeviceRecord, SmartAttribute } from "@/types" import type { SmartDeviceRecord, SmartAttribute } from "@/types"
import { import {
formatBytes, formatBytes,
@@ -146,7 +146,9 @@ export const createColumns = (
{ {
accessorKey: "model", accessorKey: "model",
sortingFn: (a, b) => a.original.model.localeCompare(b.original.model), sortingFn: (a, b) => a.original.model.localeCompare(b.original.model),
header: ({ column }) => <HeaderButton column={column} name={t`Model`} Icon={Box} />, header: ({ column }) => (
<HeaderButton column={column} name={t({ message: "Model", comment: "Device model" })} Icon={Box} />
),
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div <div
className="max-w-48 truncate ms-1" className="max-w-48 truncate ms-1"
@@ -490,7 +492,7 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
const columns = createColumns(longestName, longestModel, longestDevice) const columns = createColumns(longestName, longestModel, longestDevice)
const baseColumns = systemId ? columns.filter((col) => col.id !== "system") : columns const baseColumns = systemId ? columns.filter((col) => col.id !== "system") : columns
return [...baseColumns, actionColumn] return isReadOnlyUser() ? baseColumns : [...baseColumns, actionColumn]
}, [systemId, actionColumn, longestName, longestModel, longestDevice]) }, [systemId, actionColumn, longestName, longestModel, longestDevice])
const table = useReactTable({ const table = useReactTable({
@@ -532,9 +534,9 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
return ( return (
<div> <div>
<Card className="p-6 @container w-full"> <Card className="@container w-full px-3 py-5 sm:py-6 sm:px-6">
<CardHeader className="p-0 mb-4"> <CardHeader className="p-0 mb-3 sm:mb-4">
<div className="grid md:flex gap-5 w-full items-end"> <div className="grid md:flex gap-x-5 gap-y-3 w-full items-end">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2">S.M.A.R.T.</CardTitle> <CardTitle className="mb-2">S.M.A.R.T.</CardTitle>
<CardDescription className="flex"> <CardDescription className="flex">
@@ -620,11 +622,13 @@ const SmartDevicesTable = memo(function SmartDevicesTable({
return <SmartDeviceTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} /> return <SmartDeviceTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} />
}) })
) : ( ) : (
<TableRow> <TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<TableCell colSpan={colLength} className="h-24 text-center pointer-events-none"> {data ? (
{data ? t`No results.` : <LoaderCircleIcon className="animate-spin size-10 opacity-60 mx-auto" />} <Trans>No results.</Trans>
) : (
<LoaderCircleIcon className="animate-spin size-10 opacity-60 mx-auto" />
)}
</TableCell> </TableCell>
</TableRow>
)} )}
</TableBody> </TableBody>
</table> </table>
@@ -636,7 +640,6 @@ const SmartDevicesTable = memo(function SmartDevicesTable({
function SmartTableHead({ table }: { table: TableType<SmartDeviceRecord> }) { function SmartTableHead({ table }: { table: TableType<SmartDeviceRecord> }) {
return ( return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2"> <TableHeader className="sticky top-0 z-50 w-full border-b-2">
<div className="absolute -top-2 left-0 w-full h-4 bg-table-header z-50"></div>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (

View File

@@ -0,0 +1,346 @@
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { subscribeKeys } from "nanostores"
import { useEffect, useMemo, useRef, useState } from "react"
import { useContainerChartConfigs } from "@/components/charts/hooks"
import { pb } from "@/lib/api"
import { SystemStatus } from "@/lib/enums"
import {
$allSystemsById,
$allSystemsByName,
$chartTime,
$containerFilter,
$direction,
$maxValues,
$systems,
$userSettings,
} from "@/lib/stores"
import { chartTimeData, listen, parseSemVer, useBrowserStorage } from "@/lib/utils"
import type {
ChartData,
ContainerStatsRecord,
SystemDetailsRecord,
SystemInfo,
SystemRecord,
SystemStats,
SystemStatsRecord,
} from "@/types"
import { $router, navigate } from "../../router"
import { appendData, cache, getStats, getTimeData, makeContainerData, makeContainerPoint } from "./chart-data"
export type SystemData = ReturnType<typeof useSystemData>
export function useSystemData(id: string) {
const direction = useStore($direction)
const systems = useStore($systems)
const chartTime = useStore($chartTime)
const maxValues = useStore($maxValues)
const [grid, setGrid] = useBrowserStorage("grid", true)
const [displayMode, setDisplayMode] = useBrowserStorage<"default" | "tabs">("displayMode", "default")
const [activeTab, setActiveTabRaw] = useState("core")
const [mountedTabs, setMountedTabs] = useState(() => new Set<string>(["core"]))
const tabsRef = useRef<string[]>(["core", "disk"])
function setActiveTab(tab: string) {
setActiveTabRaw(tab)
setMountedTabs((prev) => (prev.has(tab) ? prev : new Set([...prev, tab])))
}
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
const persistChartTime = useRef(false)
const statsRequestId = useRef(0)
const [chartLoading, setChartLoading] = useState(true)
const [details, setDetails] = useState<SystemDetailsRecord>({} as SystemDetailsRecord)
useEffect(() => {
return () => {
if (!persistChartTime.current) {
$chartTime.set($userSettings.get().chartTime)
}
persistChartTime.current = false
setSystemStats([])
setContainerData([])
setDetails({} as SystemDetailsRecord)
$containerFilter.set("")
}
}, [id])
// find matching system and update when it changes
useEffect(() => {
if (!systems.length) {
return
}
// allow old system-name slug to work
const store = $allSystemsById.get()[id] ? $allSystemsById : $allSystemsByName
return subscribeKeys(store, [id], (newSystems) => {
const sys = newSystems[id]
if (sys) {
setSystem(sys)
document.title = `${sys?.name} / Beszel`
}
})
}, [id, systems.length])
// hide 1m chart time if system agent version is less than 0.13.0
useEffect(() => {
if (parseSemVer(system?.info?.v) < parseSemVer("0.13.0")) {
$chartTime.set("1h")
}
}, [system?.info?.v])
// fetch system details
useEffect(() => {
// if system.info.m exists, agent is old version without system details
if (!system.id || system.info?.m) {
return
}
pb.collection<SystemDetailsRecord>("system_details")
.getOne(system.id, {
fields: "hostname,kernel,cores,threads,cpu,os,os_name,arch,memory,podman",
headers: {
"Cache-Control": "public, max-age=60",
},
})
.then(setDetails)
}, [system.id])
// subscribe to realtime metrics if chart time is 1m
useEffect(() => {
let unsub = () => {}
if (!system.id || chartTime !== "1m") {
return
}
if (system.status !== SystemStatus.Up || parseSemVer(system?.info?.v).minor < 13) {
$chartTime.set("1h")
return
}
let isFirst = true
pb.realtime
.subscribe(
`rt_metrics`,
(data: { container: ContainerStatsRecord[]; info: SystemInfo; stats: SystemStats }) => {
const now = Date.now()
const statsPoint = { created: now, stats: data.stats } as SystemStatsRecord
const containerPoint =
data.container?.length > 0
? makeContainerPoint(now, data.container as unknown as ContainerStatsRecord["stats"])
: null
// on first message, make sure we clear out data from other time periods
if (isFirst) {
isFirst = false
setSystemStats([statsPoint])
setContainerData(containerPoint ? [containerPoint] : [])
return
}
setSystemStats((prev) => appendData(prev, [statsPoint], 1000, 60))
if (containerPoint) {
setContainerData((prev) => appendData(prev, [containerPoint], 1000, 60))
}
},
{ query: { system: system.id } }
)
.then((us) => {
unsub = us
})
return () => {
unsub?.()
}
}, [chartTime, system.id])
const agentVersion = useMemo(() => parseSemVer(system?.info?.v), [system?.info?.v])
const chartData: ChartData = useMemo(() => {
const lastCreated = Math.max(
(systemStats.at(-1)?.created as number) ?? 0,
(containerData.at(-1)?.created as number) ?? 0
)
return {
systemStats,
containerData,
chartTime,
orientation: direction === "rtl" ? "right" : "left",
...getTimeData(chartTime, lastCreated),
agentVersion,
}
}, [systemStats, containerData, direction])
// Share chart config computation for all container charts
const containerChartConfigs = useContainerChartConfigs(containerData)
// get stats when system "changes." (Not just system to system,
// also when new info comes in via systemManager realtime connection, indicating an update)
useEffect(() => {
if (!system.id || !chartTime || chartTime === "1m") {
return
}
const systemId = system.id
const { expectedInterval } = chartTimeData[chartTime]
const ss_cache_key = `${systemId}_${chartTime}_system_stats`
const cs_cache_key = `${systemId}_${chartTime}_container_stats`
const requestId = ++statsRequestId.current
const cachedSystemStats = cache.get(ss_cache_key) as SystemStatsRecord[] | undefined
const cachedContainerData = cache.get(cs_cache_key) as ChartData["containerData"] | undefined
// Render from cache immediately if available
if (cachedSystemStats?.length) {
setSystemStats(cachedSystemStats)
setContainerData(cachedContainerData || [])
setChartLoading(false)
// Skip the fetch if the latest cached point is recent enough that no new point is expected yet
const lastCreated = cachedSystemStats.at(-1)?.created as number | undefined
if (lastCreated && Date.now() - lastCreated < expectedInterval * 0.9) {
return
}
} else {
setChartLoading(true)
}
Promise.allSettled([
getStats<SystemStatsRecord>("system_stats", systemId, chartTime),
getStats<ContainerStatsRecord>("container_stats", systemId, chartTime),
]).then(([systemStats, containerStats]) => {
// If another request has been made since this one, ignore the results
if (requestId !== statsRequestId.current) {
return
}
setChartLoading(false)
// make new system stats
let systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]
if (systemStats.status === "fulfilled" && systemStats.value.length) {
systemData = appendData(systemData, systemStats.value, expectedInterval, 100)
cache.set(ss_cache_key, systemData)
}
setSystemStats(systemData)
// make new container stats
let containerData = (cache.get(cs_cache_key) || []) as ChartData["containerData"]
if (containerStats.status === "fulfilled" && containerStats.value.length) {
containerData = appendData(containerData, makeContainerData(containerStats.value), expectedInterval, 100)
cache.set(cs_cache_key, containerData)
}
setContainerData(containerData)
})
}, [system, chartTime])
// keyboard navigation between systems
// in tabs mode: arrow keys switch tabs, shift+arrow switches systems
// in default mode: arrow keys switch systems
useEffect(() => {
if (!systems.length) {
return
}
const handleKeyUp = (e: KeyboardEvent) => {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.ctrlKey ||
e.metaKey ||
e.altKey
) {
return
}
const isLeft = e.key === "ArrowLeft" || e.key === "h"
const isRight = e.key === "ArrowRight" || e.key === "l"
if (!isLeft && !isRight) {
return
}
// in tabs mode, plain arrows switch tabs, shift+arrows switch systems
if (displayMode === "tabs") {
if (!e.shiftKey) {
// skip if focused in tablist (Radix handles it natively)
if (e.target instanceof HTMLElement && e.target.closest('[role="tablist"]')) {
return
}
const tabs = tabsRef.current
const currentIdx = tabs.indexOf(activeTab)
const nextIdx = isLeft ? (currentIdx - 1 + tabs.length) % tabs.length : (currentIdx + 1) % tabs.length
setActiveTab(tabs[nextIdx])
return
}
} else if (e.shiftKey) {
return
}
const currentIndex = systems.findIndex((s) => s.id === id)
if (currentIndex === -1 || systems.length <= 1) {
return
}
if (isLeft) {
const prevIndex = (currentIndex - 1 + systems.length) % systems.length
persistChartTime.current = true
setActiveTabRaw("core")
setMountedTabs(new Set(["core"]))
return navigate(getPagePath($router, "system", { id: systems[prevIndex].id }))
}
if (isRight) {
const nextIndex = (currentIndex + 1) % systems.length
persistChartTime.current = true
setActiveTabRaw("core")
setMountedTabs(new Set(["core"]))
return navigate(getPagePath($router, "system", { id: systems[nextIndex].id }))
}
}
return listen(document, "keyup", handleKeyUp)
}, [id, systems, displayMode, activeTab])
// derived values
const isLongerChart = !["1m", "1h"].includes(chartTime)
const showMax = maxValues && isLongerChart
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
const lastGpus = systemStats.at(-1)?.stats?.g
const isPodman = details?.podman ?? system.info?.p ?? false
let hasGpuData = false
let hasGpuEnginesData = false
let hasGpuPowerData = false
if (lastGpus) {
hasGpuData = Object.keys(lastGpus).length > 0
for (let i = 0; i < systemStats.length && (!hasGpuEnginesData || !hasGpuPowerData); i++) {
const gpus = systemStats[i].stats?.g
if (!gpus) continue
for (const id in gpus) {
if (!hasGpuEnginesData && gpus[id].e !== undefined) {
hasGpuEnginesData = true
}
if (!hasGpuPowerData && (gpus[id].p !== undefined || gpus[id].pp !== undefined)) {
hasGpuPowerData = true
}
if (hasGpuEnginesData && hasGpuPowerData) break
}
}
}
return {
system,
systemStats,
containerData,
chartData,
containerChartConfigs,
details,
grid,
setGrid,
displayMode,
setDisplayMode,
activeTab,
setActiveTab,
mountedTabs,
tabsRef,
maxValues,
isLongerChart,
showMax,
dataEmpty,
isPodman,
lastGpus,
hasGpuData,
hasGpuEnginesData,
hasGpuPowerData,
}
}

View File

@@ -18,7 +18,7 @@ import { listenKeys } from "nanostores"
import { memo, type ReactNode, useEffect, useMemo, useRef, useState } from "react" import { memo, type ReactNode, useEffect, useMemo, useRef, useState } from "react"
import { getStatusColor, systemdTableCols } from "@/components/systemd-table/systemd-table-columns" import { getStatusColor, systemdTableCols } from "@/components/systemd-table/systemd-table-columns"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet" import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
@@ -154,20 +154,20 @@ export default function SystemdTable({ systemId }: { systemId?: string }) {
} }
return ( return (
<Card className="p-6 @container w-full"> <Card className="@container w-full px-3 py-5 sm:py-6 sm:px-6">
<CardHeader className="p-0 mb-4"> <CardHeader className="p-0 mb-3 sm:mb-4">
<div className="grid md:flex gap-5 w-full items-end"> <div className="grid md:flex gap-x-5 gap-y-3 w-full items-end">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2"> <CardTitle className="mb-2">
<Trans>Systemd Services</Trans> <Trans>Systemd Services</Trans>
</CardTitle> </CardTitle>
<CardDescription className="flex items-center"> <div className="text-sm text-muted-foreground flex items-center flex-wrap">
<Trans>Total: {data.length}</Trans> <Trans>Total: {data.length}</Trans>
<Separator orientation="vertical" className="h-4 mx-2 bg-primary/40" /> <Separator orientation="vertical" className="h-4 mx-2 bg-primary/40" />
<Trans>Failed: {statusTotals[ServiceStatus.Failed]}</Trans> <Trans>Failed: {statusTotals[ServiceStatus.Failed]}</Trans>
<Separator orientation="vertical" className="h-4 mx-2 bg-primary/40" /> <Separator orientation="vertical" className="h-4 mx-2 bg-primary/40" />
<Trans>Updated every 10 minutes.</Trans> <Trans>Updated every 10 minutes.</Trans>
</CardDescription> </div>
</div> </div>
<Input <Input
placeholder={t`Filter...`} placeholder={t`Filter...`}
@@ -614,7 +614,6 @@ function SystemdSheet({
function SystemdTableHead({ table }: { table: TableType<SystemdRecord> }) { function SystemdTableHead({ table }: { table: TableType<SystemdRecord> }) {
return ( return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2"> <TableHeader className="sticky top-0 z-50 w-full border-b-2">
<div className="absolute -top-2 left-0 w-full h-4 bg-table-header z-50"></div>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {

View File

@@ -134,8 +134,8 @@ export default function SystemsTable() {
const CardHead = useMemo(() => { const CardHead = useMemo(() => {
return ( return (
<CardHeader className="pb-4.5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1"> <CardHeader className="p-0 mb-3 sm:mb-4">
<div className="grid md:flex gap-5 w-full items-end"> <div className="grid md:flex gap-x-5 gap-y-3 w-full items-end">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2"> <CardTitle className="mb-2">
<Trans>All Systems</Trans> <Trans>All Systems</Trans>
@@ -302,9 +302,8 @@ export default function SystemsTable() {
]) ])
return ( return (
<Card> <Card className="w-full px-3 py-5 sm:py-6 sm:px-6">
{CardHead} {CardHead}
<div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
{viewMode === "table" ? ( {viewMode === "table" ? (
// table layout // table layout
<div className="rounded-md"> <div className="rounded-md">
@@ -324,7 +323,6 @@ export default function SystemsTable() {
)} )}
</div> </div>
)} )}
</div>
</Card> </Card>
) )
} }
@@ -391,7 +389,6 @@ function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
const { t } = useLingui() const { t } = useLingui()
return ( return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2"> <TableHeader className="sticky top-0 z-50 w-full border-b-2">
<div className="absolute -top-2 left-0 w-full h-4 bg-table-header z-50"></div>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
@@ -463,14 +460,14 @@ const SystemCard = memo(
} }
)} )}
> >
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60"> <CardHeader className="py-1 ps-4 pe-2 bg-muted/30 border-b border-border/60">
<div className="flex items-center gap-2 w-full overflow-hidden"> <div className="flex items-center gap-1 w-full overflow-hidden">
<CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5"> <h3 className="text-primary/90 min-w-0 flex-1 gap-2.5 font-semibold">
<div className="flex items-center gap-2.5 min-w-0 flex-1"> <div className="flex items-center gap-2.5 min-w-0 flex-1">
<IndicatorDot system={system} /> <IndicatorDot system={system} />
<span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span> <span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span>
</div> </div>
</CardTitle> </h3>
{table.getColumn("actions")?.getIsVisible() && ( {table.getColumn("actions")?.getIsVisible() && (
<div className="flex gap-1 shrink-0 relative z-10"> <div className="flex gap-1 shrink-0 relative z-10">
<AlertButton system={system} /> <AlertButton system={system} />

View File

@@ -43,7 +43,7 @@ const AlertDialogContent = React.forwardRef<
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-2 text-center sm:text-start", className)} {...props} /> <div className={cn("grid gap-2 text-start", className)} {...props} />
) )
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = "AlertDialogHeader"

View File

@@ -18,7 +18,7 @@ CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} /> <h3 ref={ref} className={cn("text-card-title font-semibold leading-none tracking-tight", className)} {...props} />
) )
) )
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle"

View File

@@ -52,7 +52,7 @@ const DialogContent = React.forwardRef<
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 text-center sm:text-start", className)} {...props} /> <div className={cn("grid gap-1.5 text-start", className)} {...props} />
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader"

View File

@@ -25,7 +25,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 data-[state=open]:bg-accent/70", "flex select-none items-center rounded-sm px-2.5 py-1.5 text-[.95em] outline-hidden focus:bg-accent/70 data-[state=open]:bg-accent/70",
inset && "ps-8", inset && "ps-8",
className className
)} )}
@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"cursor-pointer relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "cursor-pointer relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-[.95em] outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
inset && "ps-8", inset && "ps-8",
className className
)} )}
@@ -95,7 +95,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-[.95em] outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className
)} )}
checked={checked} checked={checked}
@@ -118,7 +118,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-[.95em] outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className className
)} )}
{...props} {...props}
@@ -141,7 +141,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn("px-2.5 py-1.5 text-sm font-semibold", inset && "ps-8", className)} className={cn("px-2.5 py-1.5 text-[.95em] font-semibold", inset && "ps-8", className)}
{...props} {...props}
/> />
)) ))

View File

@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs cursor-pointer", "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs cursor-pointer hover:text-foreground",
className className
)} )}
{...props} {...props}

View File

@@ -147,6 +147,12 @@
button { button {
cursor: pointer; cursor: pointer;
} }
/* cosmetic patch for half pixel gap in table headers when scrolling content shows at top */
thead.sticky:before {
content: "";
@apply absolute -top-2 left-0 w-full h-4 bg-table-header z-50
}
} }
@utility container { @utility container {
@@ -163,6 +169,18 @@
min-width: 30.3rem; min-width: 30.3rem;
} }
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
@utility text-card-title {
@apply text-[1.4rem] sm:text-2xl;
}
.recharts-tooltip-wrapper { .recharts-tooltip-wrapper {
z-index: 51; z-index: 51;
@apply tabular-nums; @apply tabular-nums;

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n" "Language: ar\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} متاح"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "1 ساعة" msgstr "1 ساعة"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "دقيقة واحدة" msgstr "دقيقة واحدة"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 ساعة" msgstr "12 ساعة"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 دقيقة" msgstr "15 دقيقة"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 يومًا" msgstr "30 يومًا"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 دقائق" msgstr "5 دقائق"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "الحالة النشطة" msgstr "الحالة النشطة"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "إضافة {foo}" msgstr "إضافة {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "إضافة <0>نظام</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "إضافة نظام"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "إضافة رابط" msgstr "إضافة رابط"
@@ -134,6 +135,7 @@ msgstr "تعديل عرض التخطيط الرئيسي"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "مسؤول" msgstr "مسؤول"
@@ -163,6 +165,7 @@ msgstr "التنبيهات"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "جميع الحاويات" msgstr "جميع الحاويات"
@@ -188,11 +191,11 @@ msgstr "هل أنت متأكد؟"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "النسخ التلقائي يتطلب سياقًا آمنًا." msgstr "النسخ التلقائي يتطلب سياقًا آمنًا."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "متوسط" msgstr "متوسط"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "متوسط استخدام وحدة المعالجة المركزية للحاويات" msgstr "متوسط استخدام وحدة المعالجة المركزية للحاويات"
@@ -206,20 +209,29 @@ msgstr "المتوسط ينخفض أقل من <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "المتوسط يتجاوز <0>{value}{0}</0>" msgstr "المتوسط يتجاوز <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "متوسط عدد عمليات الإدخال والإخراج التي تنتظر خدمتها"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "متوسط ​​استهلاك طاقة وحدة معالجة الرسوميات" msgstr "متوسط ​​استهلاك طاقة وحدة معالجة الرسوميات"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "متوسط الوقت من الانتظار في الدور حتى الإتمام لكل عملية"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "متوسط استخدام وحدة المعالجة المركزية على مستوى النظام" msgstr "متوسط استخدام وحدة المعالجة المركزية على مستوى النظام"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "متوسط ​​استخدام {0}" msgstr "متوسط ​​استخدام {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "متوسط استغلال محركات GPU" msgstr "متوسط استغلال محركات GPU"
@@ -228,7 +240,7 @@ msgstr "متوسط استغلال محركات GPU"
msgid "Backups" msgid "Backups"
msgstr "النسخ الاحتياطية" msgstr "النسخ الاحتياطية"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "عرض النطاق الترددي" msgstr "عرض النطاق الترددي"
@@ -238,7 +250,7 @@ msgstr "عرض النطاق الترددي"
msgid "Bat" msgid "Bat"
msgstr "بطارية" msgstr "بطارية"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "البطارية" msgstr "البطارية"
@@ -288,7 +300,7 @@ msgstr "حالة التمهيد"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "بايت (كيلوبايت/ثانية، ميجابايت/ثانية، جيجابايت/ثانية)" msgstr "بايت (كيلوبايت/ثانية، ميجابايت/ثانية، جيجابايت/ثانية)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "ذاكرة التخزين المؤقت / المخازن المؤقتة" msgstr "ذاكرة التخزين المؤقت / المخازن المؤقتة"
@@ -334,7 +346,7 @@ msgstr "تغيير وحدات عرض المقاييس."
msgid "Change general application options." msgid "Change general application options."
msgstr "تغيير خيارات التطبيق العامة." msgstr "تغيير خيارات التطبيق العامة."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "الشحن" msgstr "الشحن"
@@ -347,6 +359,10 @@ msgstr "قيد الشحن"
msgid "Chart options" msgid "Chart options"
msgstr "خيارات الرسم البياني" msgstr "خيارات الرسم البياني"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "عرض الرسم البياني"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "تحقق من {email} للحصول على رابط إعادة التعيين." msgstr "تحقق من {email} للحصول على رابط إعادة التعيين."
@@ -407,6 +423,10 @@ msgstr "التعارضات"
msgid "Connection is down" msgid "Connection is down"
msgstr "الاتصال مقطوع" msgstr "الاتصال مقطوع"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "الحاويات"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "نسخ متغيرات البيئة" msgstr "نسخ متغيرات البيئة"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "نسخ من"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "نسخ المضيف" msgstr "نسخ المضيف"
@@ -462,6 +487,11 @@ msgstr "انسخ محتوى <0>docker-compose.yml</0> للوكيل أدناه،
msgid "Copy YAML" msgid "Copy YAML"
msgstr "نسخ YAML" msgstr "نسخ YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "النواة"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -484,8 +514,8 @@ msgstr "وقت المعالج"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "تفصيل وقت المعالج" msgstr "تفصيل وقت المعالج"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "الرفع التراكمي" msgstr "الرفع التراكمي"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "الحالة الحالية" msgstr "الحالة الحالية"
@@ -531,6 +561,11 @@ msgstr "الدورات"
msgid "Daily" msgid "Daily"
msgstr "يوميًا" msgstr "يوميًا"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "افتراضي"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "الفترة الزمنية الافتراضية" msgstr "الفترة الزمنية الافتراضية"
@@ -563,11 +598,12 @@ msgstr "الجهاز"
msgid "Discharging" msgid "Discharging"
msgstr "قيد التفريغ" msgstr "قيد التفريغ"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "القرص" msgstr "القرص"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "إدخال/إخراج القرص" msgstr "إدخال/إخراج القرص"
@@ -575,25 +611,30 @@ msgstr "إدخال/إخراج القرص"
msgid "Disk unit" msgid "Disk unit"
msgstr "وحدة القرص" msgstr "وحدة القرص"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "استخدام القرص" msgstr "استخدام القرص"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "استخدام القرص لـ {extraFsName}" msgstr "استخدام القرص لـ {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "عرض"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "استخدام المعالج للدوكر" msgstr "استخدام المعالج للدوكر"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "استخدام الذاكرة للدوكر" msgstr "استخدام الذاكرة للدوكر"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "إدخال/إخراج الشبكة للدوكر" msgstr "إدخال/إخراج الشبكة للدوكر"
@@ -770,7 +811,7 @@ msgstr "فشل: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "أمر FreeBSD" msgstr "أمر FreeBSD"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "ممتلئة" msgstr "ممتلئة"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "عالمي" msgstr "عالمي"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr "معالج الرسوميات"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "محركات GPU" msgstr "محركات GPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "استهلاك طاقة وحدة معالجة الرسوميات" msgstr "استهلاك طاقة وحدة معالجة الرسوميات"
@@ -826,6 +872,7 @@ msgstr "استهلاك طاقة وحدة معالجة الرسوميات"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "استخدام وحدة معالجة الرسوميات" msgstr "استخدام وحدة معالجة الرسوميات"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "شبكة" msgstr "شبكة"
@@ -864,6 +911,21 @@ msgstr "طريقة HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "طريقة HTTP: POST، GET، أو HEAD (الافتراضي: POST)" msgstr "طريقة HTTP: POST، GET، أو HEAD (الافتراضي: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "انتظار الإدخال والإخراج"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "وقت الإدخال والإخراج"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "استخدام الإدخال والإخراج"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -912,7 +974,7 @@ msgstr "دورة الحياة"
msgid "limit" msgid "limit"
msgstr "الحد" msgstr "الحد"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "متوسط التحميل" msgstr "متوسط التحميل"
@@ -941,6 +1003,7 @@ msgstr "حالة التحميل"
msgid "Loading..." msgid "Loading..."
msgstr "جاري التحميل..." msgstr "جاري التحميل..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "تسجيل الخروج" msgstr "تسجيل الخروج"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "تعليمات الإعداد اليدوي" msgstr "تعليمات الإعداد اليدوي"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "الحد الأقصى دقيقة" msgstr "الحد الأقصى دقيقة"
@@ -999,15 +1062,16 @@ msgstr "حد الذاكرة"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "ذروة الذاكرة" msgstr "ذروة الذاكرة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "استخدام الذاكرة" msgstr "استخدام الذاكرة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "استخدام الذاكرة لحاويات دوكر" msgstr "استخدام الذاكرة لحاويات دوكر"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "الموديل" msgstr "الموديل"
@@ -1025,11 +1089,11 @@ msgstr "الاسم"
msgid "Net" msgid "Net"
msgstr "الشبكة" msgstr "الشبكة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "حركة مرور الشبكة لحاويات الدوكر" msgstr "حركة مرور الشبكة لحاويات الدوكر"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "تنسيق الحمولة"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "متوسط الاستخدام لكل نواة" msgstr "متوسط الاستخدام لكل نواة"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "النسبة المئوية للوقت الذي يكون فيه القرص مشغولاً بالإدخال والإخراج"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "النسبة المئوية للوقت المقضي في كل حالة" msgstr "النسبة المئوية للوقت المقضي في كل حالة"
@@ -1220,13 +1288,18 @@ msgstr "يرجى تسجيل الدخول إلى حسابك"
msgid "Port" msgid "Port"
msgstr "المنفذ" msgstr "المنفذ"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "المنافذ"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "تشغيل الطاقة" msgstr "تشغيل الطاقة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "الاستخدام الدقيق في الوقت المسجل" msgstr "الاستخدام الدقيق في الوقت المسجل"
@@ -1243,17 +1316,24 @@ msgstr "تم بدء العملية"
msgid "Public Key" msgid "Public Key"
msgstr "المفتاح العام" msgstr "المفتاح العام"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "عمق الدور"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "ساعات الهدوء" msgstr "ساعات الهدوء"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "قراءة" msgstr "قراءة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "تم الاستلام" msgstr "تم الاستلام"
@@ -1326,6 +1406,10 @@ msgstr "تفاصيل S.M.A.R.T."
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "اختبار S.M.A.R.T. الذاتي" msgstr "اختبار S.M.A.R.T. الذاتي"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "حفظ {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي." msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي."
@@ -1335,10 +1419,6 @@ msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو
msgid "Save Settings" msgid "Save Settings"
msgstr "حفظ الإعدادات" msgstr "حفظ الإعدادات"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "احفظ النظام"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "محفوظ في قاعدة البيانات ولا ينتهي حتى تقوم بتعطيله." msgstr "محفوظ في قاعدة البيانات ولا ينتهي حتى تقوم بتعطيله."
@@ -1387,7 +1467,7 @@ msgstr "أرسل pings صادرة دورية إلى خدمة مراقبة خار
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "إرسال نبضة قلب اختبارية" msgstr "إرسال نبضة قلب اختبارية"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "تم الإرسال" msgstr "تم الإرسال"
@@ -1399,6 +1479,7 @@ msgstr "الرقم التسلسلي"
msgid "Service Details" msgid "Service Details"
msgstr "تفاصيل الخدمة" msgstr "تفاصيل الخدمة"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "الخدمات" msgstr "الخدمات"
@@ -1414,8 +1495,10 @@ msgstr "قم بتعيين متغيرات البيئة التالية على مر
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "الإعدادات" msgstr "الإعدادات"
@@ -1459,17 +1542,18 @@ msgstr "الحالة"
msgid "Sub State" msgid "Sub State"
msgstr "الحالة الفرعية" msgstr "الحالة الفرعية"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "مساحة التبديل المستخدمة من قبل النظام" msgstr "مساحة التبديل المستخدمة من قبل النظام"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "استخدام التبديل" msgstr "استخدام التبديل"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "استخدام التبديل"
msgid "System" msgid "System"
msgstr "النظام" msgstr "النظام"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "متوسط تحميل النظام مع مرور الوقت" msgstr "متوسط تحميل النظام مع مرور الوقت"
@@ -1501,6 +1585,11 @@ msgstr "يمكن إدارة الأنظمة في ملف <0>config.yml</0> داخ
msgid "Table" msgid "Table"
msgstr "جدول" msgstr "جدول"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "تبويبات"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "المهام" msgstr "المهام"
@@ -1511,7 +1600,7 @@ msgstr "المهام"
msgid "Temp" msgid "Temp"
msgstr "درجة الحرارة" msgstr "درجة الحرارة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "درجة الحرارة" msgstr "درجة الحرارة"
@@ -1520,7 +1609,7 @@ msgstr "درجة الحرارة"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "وحدة درجة الحرارة" msgstr "وحدة درجة الحرارة"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "درجات حرارة مستشعرات النظام" msgstr "درجات حرارة مستشعرات النظام"
@@ -1552,11 +1641,11 @@ msgstr "لا يمكن التراجع عن هذا الإجراء. سيؤدي ذل
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "سيؤدي هذا إلى حذف جميع السجلات المحددة من قاعدة البيانات بشكل دائم." msgstr "سيؤدي هذا إلى حذف جميع السجلات المحددة من قاعدة البيانات بشكل دائم."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "معدل نقل {extraFsName}" msgstr "معدل نقل {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "معدل نقل نظام الملفات الجذر" msgstr "معدل نقل نظام الملفات الجذر"
@@ -1568,11 +1657,6 @@ msgstr "تنسيق الوقت"
msgid "To email(s)" msgid "To email(s)"
msgstr "إلى البريد الإشباكي" msgstr "إلى البريد الإشباكي"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "تبديل الشبكة"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1610,6 +1694,11 @@ msgstr "إجمالي البيانات المستلمة لكل واجهة"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "إجمالي البيانات المرسلة لكل واجهة" msgstr "إجمالي البيانات المرسلة لكل واجهة"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1730,20 +1819,20 @@ msgstr "رفع"
msgid "Uptime" msgid "Uptime"
msgstr "مدة التشغيل" msgstr "مدة التشغيل"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "الاستخدام" msgstr "الاستخدام"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "استخدام القسم الجذر" msgstr "استخدام القسم الجذر"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "مستخدم" msgstr "مستخدم"
@@ -1752,6 +1841,11 @@ msgstr "مستخدم"
msgid "Users" msgid "Users"
msgstr "المستخدمون" msgstr "المستخدمون"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "الاستخدام"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "القيمة" msgstr "القيمة"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "عرض" msgstr "عرض"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "عرض المزيد" msgstr "عرض المزيد"
@@ -1773,7 +1868,7 @@ msgstr "عرض أحدث 200 تنبيه."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "الأعمدة الظاهرة" msgstr "الأعمدة الظاهرة"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "في انتظار وجود سجلات كافية للعرض" msgstr "في انتظار وجود سجلات كافية للعرض"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "أمر ويندوز" msgstr "أمر ويندوز"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "كتابة" msgstr "كتابة"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n" "Language: bg\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-03-28 09:32\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "Версия {0} е налична"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "1 час" msgstr "1 час"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 минута" msgstr "1 минута"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 часа" msgstr "12 часа"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 минути" msgstr "15 минути"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 дни" msgstr "30 дни"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 минути" msgstr "5 минути"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Активно състояние" msgstr "Активно състояние"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Добави {foo}" msgstr "Добави {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Добави <0>Система</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Добави система"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Добави URL" msgstr "Добави URL"
@@ -134,6 +135,7 @@ msgstr "Настройка ширината на основния макет"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Администратор" msgstr "Администратор"
@@ -163,6 +165,7 @@ msgstr "Тревоги"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Всички контейнери" msgstr "Всички контейнери"
@@ -188,11 +191,11 @@ msgstr "Сигурни ли сте?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Автоматичното копиране изисква защитен контескт." msgstr "Автоматичното копиране изисква защитен контескт."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Средно" msgstr "Средно"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Средно използване на процесора на контейнерите" msgstr "Средно използване на процесора на контейнерите"
@@ -206,20 +209,29 @@ msgstr "Средната стойност пада под <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Средната стойност надхвърля <0>{value}{0}</0>" msgstr "Средната стойност надхвърля <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Среден брой I/O операции, чакащи обслужване"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Средна консумация на ток от графични карти" msgstr "Средна консумация на ток от графични карти"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Средно време в опашката до приключване на операция"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Средно използване на процесора на цялата система" msgstr "Средно използване на процесора на цялата система"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Средно използване на {0}" msgstr "Средно използване на {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Средно използване на GPU двигатели" msgstr "Средно използване на GPU двигатели"
@@ -228,7 +240,7 @@ msgstr "Средно използване на GPU двигатели"
msgid "Backups" msgid "Backups"
msgstr "Архиви" msgstr "Архиви"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Bandwidth на мрежата" msgstr "Bandwidth на мрежата"
@@ -238,7 +250,7 @@ msgstr "Bandwidth на мрежата"
msgid "Bat" msgid "Bat"
msgstr "Бат" msgstr "Бат"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Батерия" msgstr "Батерия"
@@ -288,7 +300,7 @@ msgstr "Състояние при зареждане"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Байта (KB/s, MB/s, GB/s)" msgstr "Байта (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Кеш / Буфери" msgstr "Кеш / Буфери"
@@ -334,7 +346,7 @@ msgstr "Промяна на единиците за показване на ме
msgid "Change general application options." msgid "Change general application options."
msgstr "Смени общите опции на приложението." msgstr "Смени общите опции на приложението."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Заряд" msgstr "Заряд"
@@ -347,6 +359,10 @@ msgstr "Зареждане"
msgid "Chart options" msgid "Chart options"
msgstr "Опции на диаграмата" msgstr "Опции на диаграмата"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Ширина на графиката"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Провери {email} за линк за нулиране." msgstr "Провери {email} за линк за нулиране."
@@ -357,7 +373,7 @@ msgstr "Провери log-овете за повече информация."
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service" msgid "Check your monitoring service"
msgstr "Проверете вашата услуга за мониторинг" msgstr "Проверете мониторинг услугата си"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Check your notification service" msgid "Check your notification service"
@@ -407,6 +423,10 @@ msgstr "Конфликти"
msgid "Connection is down" msgid "Connection is down"
msgstr "Връзката е прекъсната" msgstr "Връзката е прекъсната"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Контейнери"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Копирай еnv" msgstr "Копирай еnv"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Копиране от"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Копирай хоста" msgstr "Копирай хоста"
@@ -462,6 +487,11 @@ msgstr "Копирайте съдържанието на<0>docker-compose.yml</0
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Копирай YAML" msgstr "Копирай YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Основни"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -484,8 +514,8 @@ msgstr "Време на CPU"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "Разбивка на времето на CPU" msgstr "Разбивка на времето на CPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Кумулативно качване" msgstr "Кумулативно качване"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Текущо състояние" msgstr "Текущо състояние"
@@ -531,6 +561,11 @@ msgstr "Цикли"
msgid "Daily" msgid "Daily"
msgstr "Дневно" msgstr "Дневно"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Подредба"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Времеви диапазон по подразбиране" msgstr "Времеви диапазон по подразбиране"
@@ -563,11 +598,12 @@ msgstr "Устройство"
msgid "Discharging" msgid "Discharging"
msgstr "Разреждане" msgstr "Разреждане"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Диск" msgstr "Диск"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Диск I/O" msgstr "Диск I/O"
@@ -575,25 +611,30 @@ msgstr "Диск I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "Единица за диск" msgstr "Единица за диск"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Използване на диск" msgstr "Използване на диск"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Изполване на диск от {extraFsName}" msgstr "Изполване на диск от {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Показване"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Използване на процесор от docker" msgstr "Използване на процесор от docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Изполване на памет от docker" msgstr "Изполване на памет от docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Мрежов I/O използван от docker" msgstr "Мрежов I/O използван от docker"
@@ -770,7 +811,7 @@ msgstr "Неуспешни: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD команда" msgstr "FreeBSD команда"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Пълна" msgstr "Пълна"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "Глобален" msgstr "Глобален"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "GPU двигатели" msgstr "GPU двигатели"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "Консумация на ток от графична карта" msgstr "Консумация на ток от графична карта"
@@ -826,6 +872,7 @@ msgstr "Консумация на ток от графична карта"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "Употреба на GPU" msgstr "Употреба на GPU"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Мрежово" msgstr "Мрежово"
@@ -836,7 +883,7 @@ msgstr "Здраве"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -864,6 +911,21 @@ msgstr "HTTP метод"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP метод: POST, GET или HEAD (по подразбиране: POST)" msgstr "HTTP метод: POST, GET или HEAD (по подразбиране: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O изчакване"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O време"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O натоварване"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -912,7 +974,7 @@ msgstr "Жизнен цикъл"
msgid "limit" msgid "limit"
msgstr "лимит" msgstr "лимит"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Средно натоварване" msgstr "Средно натоварване"
@@ -941,6 +1003,7 @@ msgstr "Състояние на зареждане"
msgid "Loading..." msgid "Loading..."
msgstr "Зареждане..." msgstr "Зареждане..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Изход" msgstr "Изход"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Инструкции за ръчна настройка" msgstr "Инструкции за ръчна настройка"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Максимум 1 минута" msgstr "Максимум 1 минута"
@@ -999,15 +1062,16 @@ msgstr "Лимит на памет"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Пик на памет" msgstr "Пик на памет"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Употреба на паметта" msgstr "Употреба на паметта"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Използването на памет от docker контейнерите" msgstr "Използването на памет от docker контейнерите"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Модел" msgstr "Модел"
@@ -1025,11 +1089,11 @@ msgstr "Име"
msgid "Net" msgid "Net"
msgstr "Мрежа" msgstr "Мрежа"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Мрежов трафик на docker контейнери" msgstr "Мрежов трафик на docker контейнери"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "Формат на полезния товар"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Средно използване на ядро" msgstr "Средно използване на ядро"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Процент от времето, в което дискът е зает с I/O операции"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Процент време, прекарано във всяко състояние" msgstr "Процент време, прекарано във всяко състояние"
@@ -1220,13 +1288,18 @@ msgstr "Моля влез в акаунта ти"
msgid "Port" msgid "Port"
msgstr "Порт" msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Портове"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Включване" msgstr "Включване"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Точно използване в записаното време" msgstr "Точно използване в записаното време"
@@ -1243,17 +1316,24 @@ msgstr "Процесът стартира"
msgid "Public Key" msgid "Public Key"
msgstr "Публичен ключ" msgstr "Публичен ключ"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Дълбочина на опашката"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Тихи часове" msgstr "Тихи часове"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Прочети" msgstr "Прочети"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Получени" msgstr "Получени"
@@ -1326,6 +1406,10 @@ msgstr "S.M.A.R.T. Детайли"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T. Самотест" msgstr "S.M.A.R.T. Самотест"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Запази {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл." msgstr "Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл."
@@ -1335,10 +1419,6 @@ msgstr "Запази адреса с enter или запетая. Остави
msgid "Save Settings" msgid "Save Settings"
msgstr "Запази настройките" msgstr "Запази настройките"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Запази система"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Запазен е в базата данни и не изтича, докато не го деактивирате." msgstr "Запазен е в базата данни и не изтича, докато не го деактивирате."
@@ -1377,7 +1457,7 @@ msgstr "Избери {foo}"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working." msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Изпратете единичен heartbeat пинг, за да проверите дали вашата крайна точка работи." msgstr "Изпратете единичен heartbeat пинг, за да се уверите, че крайната Ви точка работи."
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet." msgid "Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet."
@@ -1387,7 +1467,7 @@ msgstr "Изпращайте периодични изходящи пингов
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Изпращане на тестов heartbeat" msgstr "Изпращане на тестов heartbeat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Изпратени" msgstr "Изпратени"
@@ -1399,6 +1479,7 @@ msgstr "Сериен номер"
msgid "Service Details" msgid "Service Details"
msgstr "Детайли на услугата" msgstr "Детайли на услугата"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Услуги" msgstr "Услуги"
@@ -1409,13 +1490,15 @@ msgstr "Задайте процентни прагове за цветовете
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:" msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Задайте следните променливи на средата на вашия Beszel hub, за да активирате мониторинга на heartbeat:" msgstr "Задайте следните променливи на средата на вашия Beszel hub, за да активирате heartbeat мониторинг:"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Настройки" msgstr "Настройки"
@@ -1459,17 +1542,18 @@ msgstr "Статус"
msgid "Sub State" msgid "Sub State"
msgstr "Подсъстояние" msgstr "Подсъстояние"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Изполван swap от системата" msgstr "Изполван swap от системата"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Използване на swap" msgstr "Използване на swap"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "Използване на swap"
msgid "System" msgid "System"
msgstr "Система" msgstr "Система"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Средно натоварване на системата във времето" msgstr "Средно натоварване на системата във времето"
@@ -1501,6 +1585,11 @@ msgstr "Системите могат да бъдат управлявани в
msgid "Table" msgid "Table"
msgstr "Таблица" msgstr "Таблица"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Табове"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Задачи" msgstr "Задачи"
@@ -1511,7 +1600,7 @@ msgstr "Задачи"
msgid "Temp" msgid "Temp"
msgstr "Температура" msgstr "Температура"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Температура" msgstr "Температура"
@@ -1520,7 +1609,7 @@ msgstr "Температура"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Единица за температура" msgstr "Единица за температура"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Температири на системни сензори" msgstr "Температири на системни сензори"
@@ -1552,11 +1641,11 @@ msgstr "Това действие не може да бъде отменено.
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Това ще доведе до трайно изтриване на всички избрани записи от базата данни." msgstr "Това ще доведе до трайно изтриване на всички избрани записи от базата данни."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Пропускателна способност на {extraFsName}" msgstr "Пропускателна способност на {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Пропускателна способност на root файловата система" msgstr "Пропускателна способност на root файловата система"
@@ -1568,11 +1657,6 @@ msgstr "Формат на времето"
msgid "To email(s)" msgid "To email(s)"
msgstr "До имейл(ите)" msgstr "До имейл(ите)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Превключване на мрежа"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1610,6 +1694,11 @@ msgstr "Общо получени данни за всеки интерфейс"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Общо изпратени данни за всеки интерфейс" msgstr "Общо изпратени данни за всеки интерфейс"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1728,22 +1817,22 @@ msgstr "Качване"
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime" msgid "Uptime"
msgstr "Uptime" msgstr "Време на работа"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Употреба" msgstr "Употреба"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Употреба на root partition-а" msgstr "Употреба на root partition-а"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Използвани" msgstr "Използвани"
@@ -1752,6 +1841,11 @@ msgstr "Използвани"
msgid "Users" msgid "Users"
msgstr "Потребители" msgstr "Потребители"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Натоварване"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Стойност" msgstr "Стойност"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Изглед" msgstr "Изглед"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Виж повече" msgstr "Виж повече"
@@ -1773,7 +1868,7 @@ msgstr "Прегледайте последните си 200 сигнала."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Видими полета" msgstr "Видими полета"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Изчаква се за достатъчно записи за показване" msgstr "Изчаква се за достатъчно записи за показване"
@@ -1799,11 +1894,11 @@ msgstr "Webhook / Пуш нотификации"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation." msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Когато е активиран, този символ позволява на агентите да се регистрират сами без предварително създаване на система." msgstr "Когато е активиран, този токен позволява на агентите да се регистрират сами без предварително създаване на система."
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts." msgid "When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts."
msgstr "При използване на POST всеки heartbeat включва JSON полезен товар с резюме на състоянието на системата, списък на спрените системи и задействаните предупреждения." msgstr "При използване на POST, всеки heartbeat включва JSON полезен товар с резюме на състоянието на системата, списък на спрените системи и задействаните предупреждения."
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Команда Windows" msgstr "Команда Windows"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Запиши" msgstr "Запиши"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n" "Language: cs\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" "Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} k dispozici"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,9 +55,9 @@ msgid "1 hour"
msgstr "1 hodina" msgstr "1 hodina"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 minute" msgid "1 minute"
@@ -66,9 +72,9 @@ msgid "12 hours"
msgstr "12 hodin" msgstr "12 hodin"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "24 hours" msgid "24 hours"
@@ -79,9 +85,9 @@ msgid "30 days"
msgstr "30 dní" msgstr "30 dní"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 min" msgstr ""
#. Table column #. Table column
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Aktivní stav" msgstr "Aktivní stav"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Přidat {foo}" msgstr "Přidat {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Přidat <0>Systém</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Přidat systém"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Přidat URL" msgstr "Přidat URL"
@@ -134,6 +135,7 @@ msgstr "Upravit šířku hlavního rozvržení"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Administrátor" msgstr "Administrátor"
@@ -147,7 +149,7 @@ msgstr "Po nastavení proměnných prostředí restartujte hub Beszel, aby se zm
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Agent" msgid "Agent"
msgstr "Agent" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -163,6 +165,7 @@ msgstr "Výstrahy"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Všechny kontejnery" msgstr "Všechny kontejnery"
@@ -188,11 +191,11 @@ msgstr "Jste si jistý?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Automatická kopie vyžaduje zabezpečený kontext." msgstr "Automatická kopie vyžaduje zabezpečený kontext."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Průměr" msgstr "Průměr"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Průměrné využití CPU kontejnerů" msgstr "Průměrné využití CPU kontejnerů"
@@ -206,20 +209,29 @@ msgstr "Průměr klesne pod <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Průměr je vyšší než <0>{value}{0}</0>" msgstr "Průměr je vyšší než <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Průměrný počet I/O operací čekajících na vyřízení"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Průměrná spotřeba energie GPU" msgstr "Průměrná spotřeba energie GPU"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Průměrná doba od zařazení do fronty po dokončení operace"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Průměrné využití CPU v celém systému" msgstr "Průměrné využití CPU v celém systému"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Průměrné využití {0}" msgstr "Průměrné využití {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Průměrné využití GPU engine" msgstr "Průměrné využití GPU engine"
@@ -228,7 +240,7 @@ msgstr "Průměrné využití GPU engine"
msgid "Backups" msgid "Backups"
msgstr "Zálohy" msgstr "Zálohy"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Přenos" msgstr "Přenos"
@@ -236,9 +248,9 @@ msgstr "Přenos"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Baterie" msgstr "Baterie"
@@ -288,7 +300,7 @@ msgstr "Stav zavádění"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Byty (KB/s, MB/s, GB/s)" msgstr "Byty (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / vyrovnávací paměť" msgstr "Cache / vyrovnávací paměť"
@@ -334,7 +346,7 @@ msgstr "Změnit jednotky zobrazení metrik."
msgid "Change general application options." msgid "Change general application options."
msgstr "Změnit obecné nastavení aplikace." msgstr "Změnit obecné nastavení aplikace."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Nabíjení" msgstr "Nabíjení"
@@ -347,6 +359,10 @@ msgstr "Nabíjení"
msgid "Chart options" msgid "Chart options"
msgstr "Možnosti grafu" msgstr "Možnosti grafu"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Šířka grafu"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Zkontrolujte {email} pro odkaz na obnovení." msgstr "Zkontrolujte {email} pro odkaz na obnovení."
@@ -407,6 +423,10 @@ msgstr "Konflikty"
msgid "Connection is down" msgid "Connection is down"
msgstr "Připojení je nedostupné" msgstr "Připojení je nedostupné"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Kontejnery"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Kopírovat env" msgstr "Kopírovat env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopírovat z"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Kopírovat hostitele" msgstr "Kopírovat hostitele"
@@ -462,6 +487,11 @@ msgstr "Zkopírujte obsah <0>docker-compose.yml</0> pro agenta níže nebo autom
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Kopírovat YAML" msgstr "Kopírovat YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Jádro"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -484,8 +514,8 @@ msgstr "Čas CPU"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "Rozdělení času CPU" msgstr "Rozdělení času CPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Kumulativní odeslání" msgstr "Kumulativní odeslání"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Aktuální stav" msgstr "Aktuální stav"
@@ -531,6 +561,11 @@ msgstr "Cykly"
msgid "Daily" msgid "Daily"
msgstr "Denně" msgstr "Denně"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Výchozí"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Výchozí doba" msgstr "Výchozí doba"
@@ -552,7 +587,7 @@ msgstr "Popis"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
msgid "Detail" msgid "Detail"
msgstr "Detail" msgstr ""
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Device" msgid "Device"
@@ -563,37 +598,43 @@ msgstr "Zařízení"
msgid "Discharging" msgid "Discharging"
msgstr "Vybíjení" msgstr "Vybíjení"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disk" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Disk I/O" msgstr "I/O disku"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Disk unit" msgid "Disk unit"
msgstr "Disková jednotka" msgstr "Disková jednotka"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Využití disku" msgstr "Využití disku"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Využití disku {extraFsName}" msgstr "Využití disku {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Zobrazení"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Využití CPU Dockeru" msgstr "Využití CPU Dockeru"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Využití paměti Dockeru" msgstr "Využití paměti Dockeru"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Síťové I/O Dockeru" msgstr "Síťové I/O Dockeru"
@@ -770,7 +811,7 @@ msgstr "Neúspěšné: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -783,7 +824,7 @@ msgstr "Otisk"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Firmware" msgid "Firmware"
msgstr "Firmware" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD příkaz" msgstr "FreeBSD příkaz"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Plná" msgstr "Plná"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "Globální" msgstr "Globální"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "GPU enginy" msgstr "GPU enginy"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "Spotřeba energie GPU" msgstr "Spotřeba energie GPU"
@@ -826,6 +872,7 @@ msgstr "Spotřeba energie GPU"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "Využití GPU" msgstr "Využití GPU"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Mřížka" msgstr "Mřížka"
@@ -836,7 +883,7 @@ msgstr "Zdraví"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -864,6 +911,21 @@ msgstr "HTTP metoda"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metoda: POST, GET nebo HEAD (výchozí: POST)" msgstr "HTTP metoda: POST, GET nebo HEAD (výchozí: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O Čekání"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O Čas"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O Využití"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -884,7 +946,7 @@ msgstr "Neaktivní"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Interval" msgid "Interval"
msgstr "Interval" msgstr ""
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Invalid email address." msgid "Invalid email address."
@@ -910,9 +972,9 @@ msgstr "Životní cyklus"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "limit" msgid "limit"
msgstr "limit" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Průměrné vytížení" msgstr "Průměrné vytížení"
@@ -941,6 +1003,7 @@ msgstr "Stav načtení"
msgid "Loading..." msgid "Loading..."
msgstr "Načítání..." msgstr "Načítání..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Odhlásit" msgstr "Odhlásit"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Pokyny k manuálnímu nastavení" msgstr "Pokyny k manuálnímu nastavení"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Max. 1 min" msgstr "Max. 1 min"
@@ -999,18 +1062,19 @@ msgstr "Limit paměti"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Špička paměti" msgstr "Špička paměti"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Využití paměti" msgstr "Využití paměti"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Využití paměti docker kontejnerů" msgstr "Využití paměti docker kontejnerů"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Model" msgstr ""
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
@@ -1025,11 +1089,11 @@ msgstr "Název"
msgid "Net" msgid "Net"
msgstr "Síť" msgstr "Síť"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Síťový provoz kontejnerů docker" msgstr "Síťový provoz kontejnerů docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "Formát payloadu"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Průměrné využití na jádro" msgstr "Průměrné využití na jádro"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Procento času, po který je disk zaneprázdněn I/O operacemi"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Procento času strávěného v každém stavu" msgstr "Procento času strávěného v každém stavu"
@@ -1218,15 +1286,20 @@ msgstr "Přihlaste se prosím k vašemu účtu"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
msgstr "Port" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Porty"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Zapnutí" msgstr "Zapnutí"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Přesné využití v zaznamenaném čase" msgstr "Přesné využití v zaznamenaném čase"
@@ -1243,17 +1316,24 @@ msgstr "Proces spuštěn"
msgid "Public Key" msgid "Public Key"
msgstr "Veřejný klíč" msgstr "Veřejný klíč"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Hloubka fronty"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Tiché hodiny" msgstr "Tiché hodiny"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Číst" msgstr "Číst"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Přijato" msgstr "Přijato"
@@ -1326,6 +1406,10 @@ msgstr "S.M.A.R.T. Detaily"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T. Vlastní test" msgstr "S.M.A.R.T. Vlastní test"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Uložit {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mailových oznámení ponechte prázdné pole." msgstr "Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mailových oznámení ponechte prázdné pole."
@@ -1335,10 +1419,6 @@ msgstr "Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mai
msgid "Save Settings" msgid "Save Settings"
msgstr "Uložit nastavení" msgstr "Uložit nastavení"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Uložit systém"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Uložen v databázi a nevyprší, dokud jej nezablokujete." msgstr "Uložen v databázi a nevyprší, dokud jej nezablokujete."
@@ -1387,7 +1467,7 @@ msgstr "Odesílejte periodické odchozí pingy na externí monitorovací službu
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Odeslat testovací heartbeat" msgstr "Odeslat testovací heartbeat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Odeslat" msgstr "Odeslat"
@@ -1399,6 +1479,7 @@ msgstr "Sériové číslo"
msgid "Service Details" msgid "Service Details"
msgstr "Detaily služby" msgstr "Detaily služby"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Služby" msgstr "Služby"
@@ -1414,8 +1495,10 @@ msgstr "Pro povolení monitorování heartbeat nastavte na hubu Beszel následuj
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Nastavení" msgstr "Nastavení"
@@ -1459,17 +1542,18 @@ msgstr "Stav"
msgid "Sub State" msgid "Sub State"
msgstr "Podstav" msgstr "Podstav"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Swap prostor využívaný systémem" msgstr "Swap prostor využívaný systémem"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap využití" msgstr "Swap využití"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "Swap využití"
msgid "System" msgid "System"
msgstr "Systém" msgstr "Systém"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Průměry zatížení systému v průběhu času" msgstr "Průměry zatížení systému v průběhu času"
@@ -1501,6 +1585,11 @@ msgstr "Systémy lze spravovat v souboru <0>config.yml</0> uvnitř datového adr
msgid "Table" msgid "Table"
msgstr "Tabulka" msgstr "Tabulka"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Karty"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Úlohy" msgstr "Úlohy"
@@ -1511,7 +1600,7 @@ msgstr "Úlohy"
msgid "Temp" msgid "Temp"
msgstr "Teplota" msgstr "Teplota"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Teplota" msgstr "Teplota"
@@ -1520,7 +1609,7 @@ msgstr "Teplota"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Jednotky teploty" msgstr "Jednotky teploty"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Teploty systémových senzorů" msgstr "Teploty systémových senzorů"
@@ -1552,11 +1641,11 @@ msgstr "Tuto akci nelze vzít zpět. Tím se z databáze trvale odstraní všech
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Tímto trvale odstraníte všechny vybrané záznamy z databáze." msgstr "Tímto trvale odstraníte všechny vybrané záznamy z databáze."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Propustnost {extraFsName}" msgstr "Propustnost {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Propustnost kořenového souborového systému" msgstr "Propustnost kořenového souborového systému"
@@ -1568,11 +1657,6 @@ msgstr "Formát času"
msgid "To email(s)" msgid "To email(s)"
msgstr "Na email(y)" msgstr "Na email(y)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Přepnout mřížku"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1581,7 +1665,7 @@ msgstr "Přepnout motiv"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1610,6 +1694,11 @@ msgstr "Celkový přijatý objem dat pro každé rozhraní"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Celkový odeslaný objem dat pro každé rozhraní" msgstr "Celkový odeslaný objem dat pro každé rozhraní"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1728,22 +1817,22 @@ msgstr "Odeslání"
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime" msgid "Uptime"
msgstr "Uptime" msgstr "Doba provozu"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Využití" msgstr "Využití"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Využití kořenového oddílu" msgstr "Využití kořenového oddílu"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Využito" msgstr "Využito"
@@ -1752,6 +1841,11 @@ msgstr "Využito"
msgid "Users" msgid "Users"
msgstr "Uživatelé" msgstr "Uživatelé"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Využití"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Hodnota" msgstr "Hodnota"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Zobrazení" msgstr "Zobrazení"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Zobrazit více" msgstr "Zobrazit více"
@@ -1773,7 +1868,7 @@ msgstr "Zobrazit vašich 200 nejnovějších upozornění."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Viditelné sloupce" msgstr "Viditelné sloupce"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Čeká se na dostatek záznamů k zobrazení" msgstr "Čeká se na dostatek záznamů k zobrazení"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Windows příkaz" msgstr "Windows příkaz"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Psát" msgstr "Psát"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n" "Language: da\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} tilgængelig"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "1 time" msgstr "1 time"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 minut" msgstr "1 minut"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 timer" msgstr "12 timer"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 minutter" msgstr "15 minutter"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 dage" msgstr "30 dage"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 minutter" msgstr "5 minutter"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Aktiv tilstand" msgstr "Aktiv tilstand"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Tilføj {foo}" msgstr "Tilføj {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Tilføj <0>System</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Tilføj system"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Tilføj URL" msgstr "Tilføj URL"
@@ -134,6 +135,7 @@ msgstr "Juster bredden af hovedlayoutet"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Administrator" msgstr "Administrator"
@@ -147,7 +149,7 @@ msgstr "Efter indstilling af miljøvariablerne skal du genstarte din Beszel-hub
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Agent" msgid "Agent"
msgstr "Agent" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -163,6 +165,7 @@ msgstr "Alarmer"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Alle containere" msgstr "Alle containere"
@@ -188,11 +191,11 @@ msgstr "Er du sikker?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Automatisk kopiering kræver en sikker kontekst." msgstr "Automatisk kopiering kræver en sikker kontekst."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Gennemsnitlig" msgstr "Gennemsnitlig"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Gennemsnitlig CPU udnyttelse af containere" msgstr "Gennemsnitlig CPU udnyttelse af containere"
@@ -206,20 +209,29 @@ msgstr "Gennemsnit falder under <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Gennemsnittet overstiger <0>{value}{0}</0>" msgstr "Gennemsnittet overstiger <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Gennemsnitligt antal I/O-operationer, der venter på at blive betjent"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Gennemsnitligt strømforbrug for GPU'er" msgstr "Gennemsnitligt strømforbrug for GPU'er"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Gennemsnitlig tid fra kø til færdiggørelse pr. operation"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Gennemsnitlig systembaseret CPU-udnyttelse" msgstr "Gennemsnitlig systembaseret CPU-udnyttelse"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Gennemsnitlig udnyttelse af {0}" msgstr "Gennemsnitlig udnyttelse af {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Gennemsnitlig udnyttelse af GPU-enheder" msgstr "Gennemsnitlig udnyttelse af GPU-enheder"
@@ -228,7 +240,7 @@ msgstr "Gennemsnitlig udnyttelse af GPU-enheder"
msgid "Backups" msgid "Backups"
msgstr "Sikkerhedskopier" msgstr "Sikkerhedskopier"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Båndbredde" msgstr "Båndbredde"
@@ -236,9 +248,9 @@ msgstr "Båndbredde"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Batteri" msgstr "Batteri"
@@ -277,7 +289,7 @@ msgstr "Binær"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)" msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bits (Kbps, Mbps, Gbps)" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Boot state" msgid "Boot state"
@@ -286,9 +298,9 @@ msgstr "Opstartstilstand"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)" msgstr ""
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / Buffere" msgstr "Cache / Buffere"
@@ -324,7 +336,7 @@ msgstr "Forsigtig - muligt tab af data"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -334,7 +346,7 @@ msgstr "Ændre viste enheder for målinger."
msgid "Change general application options." msgid "Change general application options."
msgstr "Skift generelle applikationsindstillinger." msgstr "Skift generelle applikationsindstillinger."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Opladning" msgstr "Opladning"
@@ -347,6 +359,10 @@ msgstr "Oplader"
msgid "Chart options" msgid "Chart options"
msgstr "Diagrammuligheder" msgstr "Diagrammuligheder"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Grafbredde"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Tjek {email} for et nulstillingslink." msgstr "Tjek {email} for et nulstillingslink."
@@ -407,6 +423,10 @@ msgstr "Konflikter"
msgid "Connection is down" msgid "Connection is down"
msgstr "Forbindelsen er nede" msgstr "Forbindelsen er nede"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Containere"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Kopier miljø" msgstr "Kopier miljø"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopier fra"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Kopier vært" msgstr "Kopier vært"
@@ -462,11 +487,16 @@ msgstr "Kopier <0>docker-compose.yml</0> indholdet for agenten nedenfor, eller r
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Kopier YAML" msgstr "Kopier YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Kerne"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -474,7 +504,7 @@ msgstr "CPU-kerner"
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
msgid "CPU Peak" msgid "CPU Peak"
msgstr "CPU Peak" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "CPU time" msgid "CPU time"
@@ -484,8 +514,8 @@ msgstr "CPU tid"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "CPU-tidsfordeling" msgstr "CPU-tidsfordeling"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Kumulativ upload" msgstr "Kumulativ upload"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Nuværende tilstand" msgstr "Nuværende tilstand"
@@ -531,6 +561,11 @@ msgstr "Cykler"
msgid "Daily" msgid "Daily"
msgstr "Dagligt" msgstr "Dagligt"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Standard"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Standard tidsperiode" msgstr "Standard tidsperiode"
@@ -563,37 +598,43 @@ msgstr "Enhed"
msgid "Discharging" msgid "Discharging"
msgstr "Aflader" msgstr "Aflader"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disk" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Disk I/O" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Disk unit" msgid "Disk unit"
msgstr "Diskenhed" msgstr "Diskenhed"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Diskforbrug" msgstr "Diskforbrug"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Diskforbrug af {extraFsName}" msgstr "Diskforbrug af {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Visning"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Docker CPU forbrug" msgstr "Docker CPU forbrug"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Docker Hukommelsesforbrug" msgstr "Docker Hukommelsesforbrug"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Docker Netværk I/O" msgstr "Docker Netværk I/O"
@@ -636,7 +677,7 @@ msgstr "Rediger {foo}"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx #: src/components/login/otp-forms.tsx
msgid "Email" msgid "Email"
msgstr "Email" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Email notifications" msgid "Email notifications"
@@ -731,7 +772,7 @@ msgstr "Eksporter din nuværende systemkonfiguration."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Failed" msgid "Failed"
@@ -770,12 +811,12 @@ msgstr "Mislykkedes: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "Filter..." msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint" msgid "Fingerprint"
@@ -783,7 +824,7 @@ msgstr "Fingeraftryk"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Firmware" msgid "Firmware"
msgstr "Firmware" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD kommando" msgstr "FreeBSD kommando"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Fuldt opladt" msgstr "Fuldt opladt"
@@ -812,13 +854,17 @@ msgstr "Generelt"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Global" msgid "Global"
msgstr "Global" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "GPU-enheder" msgstr "GPU-enheder"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "GPU Strøm Træk" msgstr "GPU Strøm Træk"
@@ -826,6 +872,7 @@ msgstr "GPU Strøm Træk"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "GPU-forbrug" msgstr "GPU-forbrug"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Gitter" msgstr "Gitter"
@@ -836,7 +883,7 @@ msgstr "Sundhed"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr "Hjerteslag"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -864,6 +911,21 @@ msgstr "HTTP-metode"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP-metode: POST, GET eller HEAD (standard: POST)" msgstr "HTTP-metode: POST, GET eller HEAD (standard: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O Vent"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O Tid"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O Udnyttelse"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -884,7 +946,7 @@ msgstr "Inaktiv"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Interval" msgid "Interval"
msgstr "Interval" msgstr ""
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Invalid email address." msgid "Invalid email address."
@@ -912,7 +974,7 @@ msgstr "Livscyklus"
msgid "limit" msgid "limit"
msgstr "grænse" msgstr "grænse"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Belastning Gennemsnitlig" msgstr "Belastning Gennemsnitlig"
@@ -941,6 +1003,7 @@ msgstr "Indlæsningstilstand"
msgid "Loading..." msgid "Loading..."
msgstr "Indlæser..." msgstr "Indlæser..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Log ud" msgstr "Log ud"
@@ -958,7 +1021,7 @@ msgstr "Loginforsøg mislykkedes"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Logs" msgid "Logs"
msgstr "Logs" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table." msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Manuel opsætningsvejledning" msgstr "Manuel opsætningsvejledning"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Maks. 1 min" msgstr "Maks. 1 min"
@@ -999,18 +1062,19 @@ msgstr "Hukommelsesgrænse"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Hukommelsesspids" msgstr "Hukommelsesspids"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Hukommelsesforbrug" msgstr "Hukommelsesforbrug"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Hukommelsesforbrug af dockercontainere" msgstr "Hukommelsesforbrug af dockercontainere"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Model" msgstr ""
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
@@ -1023,13 +1087,13 @@ msgstr "Navn"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Net" msgid "Net"
msgstr "Net" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Netværkstrafik af dockercontainere" msgstr "Netværkstrafik af dockercontainere"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1152,7 +1216,7 @@ msgstr "Tidligere"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Pause" msgid "Pause"
msgstr "Pause" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Paused" msgid "Paused"
@@ -1171,13 +1235,17 @@ msgstr "Payload-format"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Gennemsnitlig udnyttelse pr. kerne" msgstr "Gennemsnitlig udnyttelse pr. kerne"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Procentdel af tiden disk-en er optaget af I/O"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Procentdel af tid brugt i hver tilstand" msgstr "Procentdel af tid brugt i hver tilstand"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent" msgid "Permanent"
msgstr "Permanent" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence" msgid "Persistence"
@@ -1218,15 +1286,20 @@ msgstr "Log venligst ind på din konto"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
msgstr "Port" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Porte"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Tænd" msgstr "Tænd"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Præcis udnyttelse på det registrerede tidspunkt" msgstr "Præcis udnyttelse på det registrerede tidspunkt"
@@ -1243,17 +1316,24 @@ msgstr "Proces startet"
msgid "Public Key" msgid "Public Key"
msgstr "Offentlig nøgle" msgstr "Offentlig nøgle"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Kødybde"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Stille timer" msgstr "Stille timer"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Læs" msgstr "Læs"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Modtaget" msgstr "Modtaget"
@@ -1304,7 +1384,7 @@ msgstr "Genoptag"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label" msgctxt "Root disk label"
msgid "Root" msgid "Root"
msgstr "Root" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token" msgid "Rotate token"
@@ -1326,6 +1406,10 @@ msgstr "S.M.A.R.T.-detaljer"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T. selvtest" msgstr "S.M.A.R.T. selvtest"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Gem {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at deaktivere e-mail-meddelelser." msgstr "Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at deaktivere e-mail-meddelelser."
@@ -1335,10 +1419,6 @@ msgstr "Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at
msgid "Save Settings" msgid "Save Settings"
msgstr "Gem indstillinger" msgstr "Gem indstillinger"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Gem system"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Gemt i databasen og udløber ikke, før du deaktiverer det." msgstr "Gemt i databasen og udløber ikke, før du deaktiverer det."
@@ -1387,7 +1467,7 @@ msgstr "Send periodiske udgående pings til en ekstern overvågningstjeneste, s
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Send test-heartbeat" msgstr "Send test-heartbeat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Sendt" msgstr "Sendt"
@@ -1399,6 +1479,7 @@ msgstr "Serienummer"
msgid "Service Details" msgid "Service Details"
msgstr "Tjenestedetaljer" msgstr "Tjenestedetaljer"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Tjenester" msgstr "Tjenester"
@@ -1414,8 +1495,10 @@ msgstr "Indstil følgende miljøvariabler på din Beszel-hub for at aktivere hea
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Indstillinger" msgstr "Indstillinger"
@@ -1453,23 +1536,24 @@ msgstr "Tilstand"
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr ""
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
msgid "Sub State" msgid "Sub State"
msgstr "Undertilstand" msgstr "Undertilstand"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Swap plads brugt af systemet" msgstr "Swap plads brugt af systemet"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap forbrug" msgstr "Swap forbrug"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1479,15 +1563,15 @@ msgstr "Swap forbrug"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "System" msgid "System"
msgstr "System" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Gennemsnitlig system belastning over tid" msgstr "Gennemsnitlig system belastning over tid"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services" msgid "Systemd Services"
msgstr "Systemd Services" msgstr ""
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Systems" msgid "Systems"
@@ -1501,6 +1585,11 @@ msgstr "Systemer kan være administreres i filen <0>config.yml</0> i din datamap
msgid "Table" msgid "Table"
msgstr "Tabel" msgstr "Tabel"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Faner"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Opgaver" msgstr "Opgaver"
@@ -1511,7 +1600,7 @@ msgstr "Opgaver"
msgid "Temp" msgid "Temp"
msgstr "Temperatur" msgstr "Temperatur"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperatur" msgstr "Temperatur"
@@ -1520,13 +1609,13 @@ msgstr "Temperatur"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Temperaturenhed" msgstr "Temperaturenhed"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Temperaturer i systemsensorer" msgstr "Temperaturer i systemsensorer"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>" msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat" msgid "Test heartbeat"
@@ -1552,11 +1641,11 @@ msgstr "Denne handling kan ikke fortrydes. Dette vil permanent slette alle aktue
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Dette vil permanent slette alle poster fra databasen." msgstr "Dette vil permanent slette alle poster fra databasen."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Gennemløb af {extraFsName}" msgstr "Gennemløb af {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Gennemløb af rodfilsystemet" msgstr "Gennemløb af rodfilsystemet"
@@ -1568,11 +1657,6 @@ msgstr "Tidsformat"
msgid "To email(s)" msgid "To email(s)"
msgstr "Til email(s)" msgstr "Til email(s)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Slå gitter til/fra"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1610,6 +1694,11 @@ msgstr "Samlet modtaget data for hver interface"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Samlet sendt data for hver interface" msgstr "Samlet sendt data for hver interface"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1671,7 +1760,7 @@ msgstr "Udløser når brugen af en disk overstiger en tærskel"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Type" msgid "Type"
msgstr "Type" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Unit file" msgid "Unit file"
@@ -1730,20 +1819,20 @@ msgstr "Overfør"
msgid "Uptime" msgid "Uptime"
msgstr "Oppetid" msgstr "Oppetid"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Forbrug" msgstr "Forbrug"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Brug af rodpartition" msgstr "Brug af rodpartition"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Brugt" msgstr "Brugt"
@@ -1752,6 +1841,11 @@ msgstr "Brugt"
msgid "Users" msgid "Users"
msgstr "Brugere" msgstr "Brugere"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Udnyttelse"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Værdi" msgstr "Værdi"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Vis" msgstr "Vis"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Se mere" msgstr "Se mere"
@@ -1773,7 +1868,7 @@ msgstr "Se dine 200 nyeste alarmer."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Synlige felter" msgstr "Synlige felter"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Venter på nok posteringer til at vise" msgstr "Venter på nok posteringer til at vise"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Windows-kommando" msgstr "Windows-kommando"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Skriv" msgstr "Skriv"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n" "Language: de\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} verfügbar"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "1 Stunde" msgstr "1 Stunde"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 Min" msgstr "1 Min"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 Stunden" msgstr "12 Stunden"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 Min" msgstr "15 Min"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 Tage" msgstr "30 Tage"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 Min" msgstr "5 Min"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Aktiver Zustand" msgstr "Aktiver Zustand"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "{foo} hinzufügen" msgstr "{foo} hinzufügen"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "<0>System</0> hinzufügen"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "System hinzufügen"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "URL hinzufügen" msgstr "URL hinzufügen"
@@ -134,8 +135,9 @@ msgstr "Breite des Hauptlayouts anpassen"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "After" msgid "After"
@@ -147,7 +149,7 @@ msgstr "Starten Sie nach dem Festlegen der Umgebungsvariablen Ihren Beszel-Hub n
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Agent" msgid "Agent"
msgstr "Agent" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -163,6 +165,7 @@ msgstr "Warnungen"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Alle Container" msgstr "Alle Container"
@@ -188,11 +191,11 @@ msgstr "Bist du sicher?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Automatisches Kopieren erfordert einen sicheren Kontext." msgstr "Automatisches Kopieren erfordert einen sicheren Kontext."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Durchschnitt" msgstr "Durchschnitt"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Durchschnittliche CPU-Auslastung der Container" msgstr "Durchschnittliche CPU-Auslastung der Container"
@@ -206,29 +209,38 @@ msgstr "Durchschnitt unterschreitet <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Durchschnitt überschreitet <0>{value}{0}</0>" msgstr "Durchschnitt überschreitet <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Durchschnittliche Anzahl der auf Bearbeitung wartenden I/O-Operationen"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Durchschnittlicher Stromverbrauch der GPUs" msgstr "Durchschnittlicher Stromverbrauch der GPUs"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Durchschnittliche Warteschlangen- bis Abschlusszeit pro Operation"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Durchschnittliche systemweite CPU-Auslastung" msgstr "Durchschnittliche systemweite CPU-Auslastung"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Durchschnittliche Auslastung von {0}" msgstr "Durchschnittliche Auslastung von {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Durchschnittliche Auslastung der GPU-Engines" msgstr "Durchschnittliche Auslastung der GPU-Engines"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Backups" msgid "Backups"
msgstr "Backups" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Bandbreite" msgstr "Bandbreite"
@@ -236,9 +248,9 @@ msgstr "Bandbreite"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr "Batt"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Batterie" msgstr "Batterie"
@@ -277,7 +289,7 @@ msgstr "Binär"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)" msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bits (Kbps, Mbps, Gbps)" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Boot state" msgid "Boot state"
@@ -286,9 +298,9 @@ msgstr "Boot-Zustand"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)" msgstr ""
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / Puffer" msgstr "Cache / Puffer"
@@ -324,7 +336,7 @@ msgstr "Vorsicht - potenzieller Datenverlust"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -334,7 +346,7 @@ msgstr "Anzeigeeinheiten der Werte ändern."
msgid "Change general application options." msgid "Change general application options."
msgstr "Allgemeine Anwendungsoptionen ändern." msgstr "Allgemeine Anwendungsoptionen ändern."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Ladung" msgstr "Ladung"
@@ -347,6 +359,10 @@ msgstr "Wird geladen"
msgid "Chart options" msgid "Chart options"
msgstr "Diagrammoptionen" msgstr "Diagrammoptionen"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Diagrammbreite"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Überprüfe {email} auf einen Link zum Zurücksetzen." msgstr "Überprüfe {email} auf einen Link zum Zurücksetzen."
@@ -407,6 +423,10 @@ msgstr "Konflikte"
msgid "Connection is down" msgid "Connection is down"
msgstr "Verbindung unterbrochen" msgstr "Verbindung unterbrochen"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Container"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Umgebungsvariablen kopieren" msgstr "Umgebungsvariablen kopieren"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopieren von"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Host kopieren" msgstr "Host kopieren"
@@ -462,11 +487,16 @@ msgstr "Kopiere den<0>docker-compose.yml</0> Inhalt für den Agent unten oder re
msgid "Copy YAML" msgid "Copy YAML"
msgstr "YAML kopieren" msgstr "YAML kopieren"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Kern"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -484,8 +514,8 @@ msgstr "CPU-Zeit"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "CPU-Zeit-Aufschlüsselung" msgstr "CPU-Zeit-Aufschlüsselung"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Kumulativer Upload" msgstr "Kumulativer Upload"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Aktueller Zustand" msgstr "Aktueller Zustand"
@@ -531,6 +561,11 @@ msgstr "Zyklen"
msgid "Daily" msgid "Daily"
msgstr "Täglich" msgstr "Täglich"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Standard"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Standardzeitraum" msgstr "Standardzeitraum"
@@ -563,11 +598,12 @@ msgstr "Gerät"
msgid "Discharging" msgid "Discharging"
msgstr "Wird entladen" msgstr "Wird entladen"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Festplatte" msgstr "Festplatte"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Festplatten-I/O" msgstr "Festplatten-I/O"
@@ -575,25 +611,30 @@ msgstr "Festplatten-I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "Festplatteneinheit" msgstr "Festplatteneinheit"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Festplattennutzung" msgstr "Festplattennutzung"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Festplattennutzung von {extraFsName}" msgstr "Festplattennutzung von {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Anzeige"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Docker-CPU-Auslastung" msgstr "Docker-CPU-Auslastung"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Docker-Arbeitsspeichernutzung" msgstr "Docker-Arbeitsspeichernutzung"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Docker-Netzwerk-I/O" msgstr "Docker-Netzwerk-I/O"
@@ -731,7 +772,7 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Failed" msgid "Failed"
@@ -770,12 +811,12 @@ msgstr "Fehlgeschlagen: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "Filter..." msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint" msgid "Fingerprint"
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD Befehl" msgstr "FreeBSD Befehl"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Voll" msgstr "Voll"
@@ -812,13 +854,17 @@ msgstr "Allgemein"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Global" msgid "Global"
msgstr "Global" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "GPU-Engines" msgstr "GPU-Engines"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "GPU-Leistungsaufnahme" msgstr "GPU-Leistungsaufnahme"
@@ -826,6 +872,7 @@ msgstr "GPU-Leistungsaufnahme"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "GPU-Auslastung" msgstr "GPU-Auslastung"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Raster" msgstr "Raster"
@@ -836,7 +883,7 @@ msgstr "Gesundheit"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -854,7 +901,7 @@ msgstr "Homebrew-Befehl"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Host / IP" msgid "Host / IP"
msgstr "Host / IP" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method" msgid "HTTP Method"
@@ -864,6 +911,21 @@ msgstr "HTTP-Methode"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP-Methode: POST, GET oder HEAD (Standard: POST)" msgstr "HTTP-Methode: POST, GET oder HEAD (Standard: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O-Wartezeit"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O-Zeit"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O-Auslastung"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -876,7 +938,7 @@ msgstr "Wenn du das Passwort für dein Administratorkonto verloren hast, kannst
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
msgctxt "Docker image" msgctxt "Docker image"
msgid "Image" msgid "Image"
msgstr "Image" msgstr ""
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive" msgid "Inactive"
@@ -912,7 +974,7 @@ msgstr "Lebenszyklus"
msgid "limit" msgid "limit"
msgstr "Limit" msgstr "Limit"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Durchschnittliche Systemlast" msgstr "Durchschnittliche Systemlast"
@@ -941,6 +1003,7 @@ msgstr "Ladezustand"
msgid "Loading..." msgid "Loading..."
msgstr "Lädt..." msgstr "Lädt..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Abmelden" msgstr "Abmelden"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Anleitung zur manuellen Einrichtung" msgstr "Anleitung zur manuellen Einrichtung"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Max 1 Min" msgstr "Max 1 Min"
@@ -999,15 +1062,16 @@ msgstr "Arbeitsspeicherlimit"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Arbeitsspeicher-Spitze" msgstr "Arbeitsspeicher-Spitze"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Arbeitsspeichernutzung" msgstr "Arbeitsspeichernutzung"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Arbeitsspeichernutzung der Docker-Container" msgstr "Arbeitsspeichernutzung der Docker-Container"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Modell" msgstr "Modell"
@@ -1018,18 +1082,18 @@ msgstr "Modell"
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Name" msgid "Name"
msgstr "Name" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Net" msgid "Net"
msgstr "Netzwerk" msgstr "Netzwerk"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Netzwerkverkehr der Docker-Container" msgstr "Netzwerkverkehr der Docker-Container"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1152,7 +1216,7 @@ msgstr "Vergangen"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Pause" msgid "Pause"
msgstr "Pause" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Paused" msgid "Paused"
@@ -1171,13 +1235,17 @@ msgstr "Payload-Format"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Durchschnittliche Auslastung pro Kern" msgstr "Durchschnittliche Auslastung pro Kern"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Prozentsatz der Zeit, in der die Festplatte mit I/O beschäftigt ist"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Prozentsatz der Zeit in jedem Zustand" msgstr "Prozentsatz der Zeit in jedem Zustand"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent" msgid "Permanent"
msgstr "Permanent" msgstr "Dauerhaft"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence" msgid "Persistence"
@@ -1218,15 +1286,20 @@ msgstr "Bitte melde dich bei deinem Konto an"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
msgstr "Port" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Eingeschaltet" msgstr "Eingeschaltet"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Genaue Nutzung zum aufgezeichneten Zeitpunkt" msgstr "Genaue Nutzung zum aufgezeichneten Zeitpunkt"
@@ -1243,17 +1316,24 @@ msgstr "Prozess gestartet"
msgid "Public Key" msgid "Public Key"
msgstr "Öffentlicher Schlüssel" msgstr "Öffentlicher Schlüssel"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Warteschlangentiefe"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Ruhezeiten" msgstr "Ruhezeiten"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Lesen" msgstr "Lesen"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Empfangen" msgstr "Empfangen"
@@ -1304,7 +1384,7 @@ msgstr "Fortsetzen"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label" msgctxt "Root disk label"
msgid "Root" msgid "Root"
msgstr "Root" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token" msgid "Rotate token"
@@ -1326,6 +1406,10 @@ msgstr "S.M.A.R.T.-Details"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T.-Selbsttest" msgstr "S.M.A.R.T.-Selbsttest"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "{foo} speichern"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren." msgstr "Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren."
@@ -1335,10 +1419,6 @@ msgstr "Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail
msgid "Save Settings" msgid "Save Settings"
msgstr "Einstellungen speichern" msgstr "Einstellungen speichern"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "System speichern"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "In der Datenbank gespeichert und läuft nicht ab, bis Sie es deaktivieren." msgstr "In der Datenbank gespeichert und läuft nicht ab, bis Sie es deaktivieren."
@@ -1387,7 +1467,7 @@ msgstr "Senden Sie regelmäßige ausgehende Pings an einen externen Überwachung
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Test-Heartbeat senden" msgstr "Test-Heartbeat senden"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Gesendet" msgstr "Gesendet"
@@ -1399,6 +1479,7 @@ msgstr "Seriennummer"
msgid "Service Details" msgid "Service Details"
msgstr "Servicedetails" msgstr "Servicedetails"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Dienste" msgstr "Dienste"
@@ -1414,8 +1495,10 @@ msgstr "Legen Sie die folgenden Umgebungsvariablen auf Ihrem Beszel-Hub fest, um
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
@@ -1453,23 +1536,24 @@ msgstr "Status"
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr ""
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
msgid "Sub State" msgid "Sub State"
msgstr "Unterzustand" msgstr "Unterzustand"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Vom System genutzter Swap-Speicher" msgstr "Vom System genutzter Swap-Speicher"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap-Nutzung" msgstr "Swap-Nutzung"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1479,9 +1563,9 @@ msgstr "Swap-Nutzung"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "System" msgid "System"
msgstr "System" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Systemlastdurchschnitt im Zeitverlauf" msgstr "Systemlastdurchschnitt im Zeitverlauf"
@@ -1501,6 +1585,11 @@ msgstr "Systeme können in einer <0>config.yml</0>-Datei im Datenverzeichnis ver
msgid "Table" msgid "Table"
msgstr "Tabelle" msgstr "Tabelle"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Aufgaben" msgstr "Aufgaben"
@@ -1511,7 +1600,7 @@ msgstr "Aufgaben"
msgid "Temp" msgid "Temp"
msgstr "Temperatur" msgstr "Temperatur"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperatur" msgstr "Temperatur"
@@ -1520,13 +1609,13 @@ msgstr "Temperatur"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Temperatureinheit" msgstr "Temperatureinheit"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Temperaturen der Systemsensoren" msgstr "Temperaturen der Systemsensoren"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>" msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat" msgid "Test heartbeat"
@@ -1552,11 +1641,11 @@ msgstr "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch werden alle
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Dadurch werden alle ausgewählten Datensätze dauerhaft aus der Datenbank gelöscht." msgstr "Dadurch werden alle ausgewählten Datensätze dauerhaft aus der Datenbank gelöscht."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Durchsatz von {extraFsName}" msgstr "Durchsatz von {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Durchsatz des Root-Dateisystems" msgstr "Durchsatz des Root-Dateisystems"
@@ -1568,11 +1657,6 @@ msgstr "Zeitformat"
msgid "To email(s)" msgid "To email(s)"
msgstr "An E-Mail(s)" msgstr "An E-Mail(s)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Raster umschalten"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1581,7 +1665,7 @@ msgstr "Darstellung umschalten"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1610,6 +1694,11 @@ msgstr "Empfangene Gesamtdatenmenge je Schnittstelle "
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Gesendete Gesamtdatenmenge je Schnittstelle" msgstr "Gesendete Gesamtdatenmenge je Schnittstelle"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr "Gesamtzeit für Lese-/Schreibvorgänge (kann 100% überschreiten)"
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1730,20 +1819,20 @@ msgstr "Hochladen"
msgid "Uptime" msgid "Uptime"
msgstr "Betriebszeit" msgstr "Betriebszeit"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Nutzung" msgstr "Nutzung"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Nutzung der Root-Partition" msgstr "Nutzung der Root-Partition"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Verwendet" msgstr "Verwendet"
@@ -1752,6 +1841,11 @@ msgstr "Verwendet"
msgid "Users" msgid "Users"
msgstr "Benutzer" msgstr "Benutzer"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Auslastung"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Wert" msgstr "Wert"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Ansicht" msgstr "Ansicht"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Mehr anzeigen" msgstr "Mehr anzeigen"
@@ -1773,7 +1868,7 @@ msgstr "Sieh dir die neusten 200 Alarme an."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Sichtbare Spalten" msgstr "Sichtbare Spalten"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Warten auf genügend Datensätze zur Anzeige" msgstr "Warten auf genügend Datensätze zur Anzeige"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Windows-Befehl" msgstr "Windows-Befehl"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Schreiben" msgstr "Schreiben"

View File

@@ -13,6 +13,12 @@ msgstr ""
"Language-Team: \n" "Language-Team: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} available"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -44,7 +50,7 @@ msgid "1 hour"
msgstr "1 hour" msgstr "1 hour"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 min" msgstr "1 min"
@@ -61,7 +67,7 @@ msgid "12 hours"
msgstr "12 hours" msgstr "12 hours"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 min" msgstr "15 min"
@@ -74,7 +80,7 @@ msgid "30 days"
msgstr "30 days" msgstr "30 days"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 min" msgstr "5 min"
@@ -102,19 +108,14 @@ msgid "Active state"
msgstr "Active state" msgstr "Active state"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Add {foo}" msgstr "Add {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Add <0>System</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Add system"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Add URL" msgstr "Add URL"
@@ -129,6 +130,7 @@ msgstr "Adjust the width of the main layout"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr "Admin"
@@ -158,6 +160,7 @@ msgstr "Alerts"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "All Containers" msgstr "All Containers"
@@ -183,11 +186,11 @@ msgstr "Are you sure?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Automatic copy requires a secure context." msgstr "Automatic copy requires a secure context."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Average" msgstr "Average"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Average CPU utilization of containers" msgstr "Average CPU utilization of containers"
@@ -201,20 +204,29 @@ msgstr "Average drops below <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Average exceeds <0>{value}{0}</0>" msgstr "Average exceeds <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Average number of I/O operations waiting to be serviced"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Average power consumption of GPUs" msgstr "Average power consumption of GPUs"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Average queue to completion time per operation"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Average system-wide CPU utilization" msgstr "Average system-wide CPU utilization"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Average utilization of {0}" msgstr "Average utilization of {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Average utilization of GPU engines" msgstr "Average utilization of GPU engines"
@@ -223,7 +235,7 @@ msgstr "Average utilization of GPU engines"
msgid "Backups" msgid "Backups"
msgstr "Backups" msgstr "Backups"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Bandwidth" msgstr "Bandwidth"
@@ -233,7 +245,7 @@ msgstr "Bandwidth"
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr "Bat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Battery" msgstr "Battery"
@@ -283,7 +295,7 @@ msgstr "Boot state"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)" msgstr "Bytes (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / Buffers" msgstr "Cache / Buffers"
@@ -329,7 +341,7 @@ msgstr "Change display units for metrics."
msgid "Change general application options." msgid "Change general application options."
msgstr "Change general application options." msgstr "Change general application options."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Charge" msgstr "Charge"
@@ -342,6 +354,10 @@ msgstr "Charging"
msgid "Chart options" msgid "Chart options"
msgstr "Chart options" msgstr "Chart options"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Chart width"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Check {email} for a reset link." msgstr "Check {email} for a reset link."
@@ -402,6 +418,10 @@ msgstr "Conflicts"
msgid "Connection is down" msgid "Connection is down"
msgstr "Connection is down" msgstr "Connection is down"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Containers"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -428,6 +448,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Copy env" msgstr "Copy env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Copy from"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Copy host" msgstr "Copy host"
@@ -457,6 +482,11 @@ msgstr "Copy the<0>docker-compose.yml</0> content for the agent below, or regist
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Copy YAML" msgstr "Copy YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Core"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -479,8 +509,8 @@ msgstr "CPU time"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "CPU Time Breakdown" msgstr "CPU Time Breakdown"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -512,7 +542,7 @@ msgid "Cumulative Upload"
msgstr "Cumulative Upload" msgstr "Cumulative Upload"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Current state" msgstr "Current state"
@@ -526,6 +556,11 @@ msgstr "Cycles"
msgid "Daily" msgid "Daily"
msgstr "Daily" msgstr "Daily"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Default"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Default time period" msgstr "Default time period"
@@ -558,11 +593,12 @@ msgstr "Device"
msgid "Discharging" msgid "Discharging"
msgstr "Discharging" msgstr "Discharging"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disk" msgstr "Disk"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Disk I/O" msgstr "Disk I/O"
@@ -570,25 +606,30 @@ msgstr "Disk I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "Disk unit" msgstr "Disk unit"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Disk Usage" msgstr "Disk Usage"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Disk usage of {extraFsName}" msgstr "Disk usage of {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Display"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Docker CPU Usage" msgstr "Docker CPU Usage"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Docker Memory Usage" msgstr "Docker Memory Usage"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Docker Network I/O" msgstr "Docker Network I/O"
@@ -765,7 +806,7 @@ msgstr "Failed: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -795,6 +836,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD command" msgstr "FreeBSD command"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Full" msgstr "Full"
@@ -810,10 +852,14 @@ msgid "Global"
msgstr "Global" msgstr "Global"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr "GPU"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "GPU Engines" msgstr "GPU Engines"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "GPU Power Draw" msgstr "GPU Power Draw"
@@ -821,6 +867,7 @@ msgstr "GPU Power Draw"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "GPU Usage" msgstr "GPU Usage"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Grid" msgstr "Grid"
@@ -859,6 +906,21 @@ msgstr "HTTP Method"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP method: POST, GET, or HEAD (default: POST)" msgstr "HTTP method: POST, GET, or HEAD (default: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O Await"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O Time"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O Utilization"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -907,7 +969,7 @@ msgstr "Lifecycle"
msgid "limit" msgid "limit"
msgstr "limit" msgstr "limit"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Load Average" msgstr "Load Average"
@@ -936,6 +998,7 @@ msgstr "Load state"
msgid "Loading..." msgid "Loading..."
msgstr "Loading..." msgstr "Loading..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Log Out" msgstr "Log Out"
@@ -973,7 +1036,7 @@ msgid "Manual setup instructions"
msgstr "Manual setup instructions" msgstr "Manual setup instructions"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Max 1 min" msgstr "Max 1 min"
@@ -994,15 +1057,16 @@ msgstr "Memory limit"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Memory Peak" msgstr "Memory Peak"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Memory Usage" msgstr "Memory Usage"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Memory usage of docker containers" msgstr "Memory usage of docker containers"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Model" msgstr "Model"
@@ -1020,11 +1084,11 @@ msgstr "Name"
msgid "Net" msgid "Net"
msgstr "Net" msgstr "Net"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Network traffic of docker containers" msgstr "Network traffic of docker containers"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1166,6 +1230,10 @@ msgstr "Payload format"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Per-core average utilization" msgstr "Per-core average utilization"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Percent of time the disk is busy with I/O"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Percentage of time spent in each state" msgstr "Percentage of time spent in each state"
@@ -1215,13 +1283,18 @@ msgstr "Please sign in to your account"
msgid "Port" msgid "Port"
msgstr "Port" msgstr "Port"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Ports"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Power On" msgstr "Power On"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Precise utilization at the recorded time" msgstr "Precise utilization at the recorded time"
@@ -1238,17 +1311,24 @@ msgstr "Process started"
msgid "Public Key" msgid "Public Key"
msgstr "Public Key" msgstr "Public Key"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Queue Depth"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Quiet Hours" msgstr "Quiet Hours"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Read" msgstr "Read"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Received" msgstr "Received"
@@ -1321,6 +1401,10 @@ msgstr "S.M.A.R.T. Details"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T. Self-Test" msgstr "S.M.A.R.T. Self-Test"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Save {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Save address using enter key or comma. Leave blank to disable email notifications." msgstr "Save address using enter key or comma. Leave blank to disable email notifications."
@@ -1330,10 +1414,6 @@ msgstr "Save address using enter key or comma. Leave blank to disable email noti
msgid "Save Settings" msgid "Save Settings"
msgstr "Save Settings" msgstr "Save Settings"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Save system"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Saved in the database and does not expire until you disable it." msgstr "Saved in the database and does not expire until you disable it."
@@ -1382,7 +1462,7 @@ msgstr "Send periodic outbound pings to an external monitoring service so you ca
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Send test heartbeat" msgstr "Send test heartbeat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Sent" msgstr "Sent"
@@ -1394,6 +1474,7 @@ msgstr "Serial Number"
msgid "Service Details" msgid "Service Details"
msgstr "Service Details" msgstr "Service Details"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Services" msgstr "Services"
@@ -1409,8 +1490,10 @@ msgstr "Set the following environment variables on your Beszel hub to enable hea
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Settings" msgstr "Settings"
@@ -1454,17 +1537,18 @@ msgstr "Status"
msgid "Sub State" msgid "Sub State"
msgstr "Sub State" msgstr "Sub State"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Swap space used by the system" msgstr "Swap space used by the system"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap Usage" msgstr "Swap Usage"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1476,7 +1560,7 @@ msgstr "Swap Usage"
msgid "System" msgid "System"
msgstr "System" msgstr "System"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "System load averages over time" msgstr "System load averages over time"
@@ -1496,6 +1580,11 @@ msgstr "Systems may be managed in a <0>config.yml</0> file inside your data dire
msgid "Table" msgid "Table"
msgstr "Table" msgstr "Table"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Tabs"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Tasks" msgstr "Tasks"
@@ -1506,7 +1595,7 @@ msgstr "Tasks"
msgid "Temp" msgid "Temp"
msgstr "Temp" msgstr "Temp"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperature" msgstr "Temperature"
@@ -1515,7 +1604,7 @@ msgstr "Temperature"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Temperature unit" msgstr "Temperature unit"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Temperatures of system sensors" msgstr "Temperatures of system sensors"
@@ -1547,11 +1636,11 @@ msgstr "This action cannot be undone. This will permanently delete all current r
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "This will permanently delete all selected records from the database." msgstr "This will permanently delete all selected records from the database."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Throughput of {extraFsName}" msgstr "Throughput of {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Throughput of root filesystem" msgstr "Throughput of root filesystem"
@@ -1563,11 +1652,6 @@ msgstr "Time format"
msgid "To email(s)" msgid "To email(s)"
msgstr "To email(s)" msgstr "To email(s)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Toggle grid"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1605,6 +1689,11 @@ msgstr "Total data received for each interface"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Total data sent for each interface" msgstr "Total data sent for each interface"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr "Total time spent on read/write (can exceed 100%)"
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1725,20 +1814,20 @@ msgstr "Upload"
msgid "Uptime" msgid "Uptime"
msgstr "Uptime" msgstr "Uptime"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Usage" msgstr "Usage"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Usage of root partition" msgstr "Usage of root partition"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Used" msgstr "Used"
@@ -1747,6 +1836,11 @@ msgstr "Used"
msgid "Users" msgid "Users"
msgstr "Users" msgstr "Users"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utilization"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Value" msgstr "Value"
@@ -1756,6 +1850,7 @@ msgid "View"
msgstr "View" msgstr "View"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "View more" msgstr "View more"
@@ -1768,7 +1863,7 @@ msgstr "View your 200 most recent alerts."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Visible Fields" msgstr "Visible Fields"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Waiting for enough records to display" msgstr "Waiting for enough records to display"
@@ -1807,8 +1902,10 @@ msgid "Windows command"
msgstr "Windows command" msgstr "Windows command"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Write" msgstr "Write"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n" "Language: es\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} disponible"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,9 +55,9 @@ msgid "1 hour"
msgstr "1 hora" msgstr "1 hora"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 minute" msgid "1 minute"
@@ -66,9 +72,9 @@ msgid "12 hours"
msgstr "12 horas" msgstr "12 horas"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "24 hours" msgid "24 hours"
@@ -79,9 +85,9 @@ msgid "30 days"
msgstr "30 días" msgstr "30 días"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 min" msgstr ""
#. Table column #. Table column
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Estado activo" msgstr "Estado activo"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Agregar {foo}" msgstr "Agregar {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Agregar <0>sistema</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Agregar sistema"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Agregar URL" msgstr "Agregar URL"
@@ -134,6 +135,7 @@ msgstr "Ajustar el ancho del diseño principal"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Administrador" msgstr "Administrador"
@@ -163,6 +165,7 @@ msgstr "Alertas"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Todos los contenedores" msgstr "Todos los contenedores"
@@ -188,11 +191,11 @@ msgstr "¿Estás seguro?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "La copia automática requiere un contexto seguro." msgstr "La copia automática requiere un contexto seguro."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Promedio" msgstr "Promedio"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Utilización promedio de CPU de los contenedores" msgstr "Utilización promedio de CPU de los contenedores"
@@ -206,20 +209,29 @@ msgstr "El promedio cae por debajo de <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "El promedio excede <0>{value}{0}</0>" msgstr "El promedio excede <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Número medio de operaciones de E/S en espera de ser atendidas"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Consumo de energía promedio de GPUs" msgstr "Consumo de energía promedio de GPUs"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Tiempo medio de cola hasta la finalización por operación"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Utilización promedio de CPU del sistema" msgstr "Utilización promedio de CPU del sistema"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Uso promedio de {0}" msgstr "Uso promedio de {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Utilización promedio de motores GPU" msgstr "Utilización promedio de motores GPU"
@@ -228,7 +240,7 @@ msgstr "Utilización promedio de motores GPU"
msgid "Backups" msgid "Backups"
msgstr "Copias de seguridad" msgstr "Copias de seguridad"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Ancho de banda" msgstr "Ancho de banda"
@@ -236,9 +248,9 @@ msgstr "Ancho de banda"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Batería" msgstr "Batería"
@@ -288,7 +300,7 @@ msgstr "Estado de arranque"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (kB/s, MB/s, GB/s)" msgstr "Bytes (kB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Caché / Buffers" msgstr "Caché / Buffers"
@@ -324,7 +336,7 @@ msgstr "Precaución - posible pérdida de datos"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -334,7 +346,7 @@ msgstr "Cambiar las unidades de visualización de las métricas."
msgid "Change general application options." msgid "Change general application options."
msgstr "Cambiar las opciones generales de la aplicación." msgstr "Cambiar las opciones generales de la aplicación."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Carga" msgstr "Carga"
@@ -347,6 +359,10 @@ msgstr "Cargando"
msgid "Chart options" msgid "Chart options"
msgstr "Opciones de gráficos" msgstr "Opciones de gráficos"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Ancho del gráfico"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Revisa {email} para un enlace de restablecimiento." msgstr "Revisa {email} para un enlace de restablecimiento."
@@ -407,6 +423,10 @@ msgstr "Conflictos"
msgid "Connection is down" msgid "Connection is down"
msgstr "La conexión está caída" msgstr "La conexión está caída"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Contenedores"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Copiar env" msgstr "Copiar env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Copiar de"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Copiar host" msgstr "Copiar host"
@@ -462,11 +487,16 @@ msgstr "Copia el contenido del<0>docker-compose.yml</0> para el agente a continu
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Copiar YAML" msgstr "Copiar YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Núcleo"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -484,8 +514,8 @@ msgstr "Tiempo de CPU"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "Desglose de tiempo de CPU" msgstr "Desglose de tiempo de CPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Carga acumulada" msgstr "Carga acumulada"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Estado actual" msgstr "Estado actual"
@@ -531,6 +561,11 @@ msgstr "Ciclos"
msgid "Daily" msgid "Daily"
msgstr "Diariamente" msgstr "Diariamente"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Predeterminado"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Periodo de tiempo predeterminado" msgstr "Periodo de tiempo predeterminado"
@@ -563,11 +598,12 @@ msgstr "Dispositivo"
msgid "Discharging" msgid "Discharging"
msgstr "Descargando" msgstr "Descargando"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disco" msgstr "Disco"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "E/S de Disco" msgstr "E/S de Disco"
@@ -575,25 +611,30 @@ msgstr "E/S de Disco"
msgid "Disk unit" msgid "Disk unit"
msgstr "Unidad de disco" msgstr "Unidad de disco"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Uso de disco" msgstr "Uso de disco"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Uso de disco de {extraFsName}" msgstr "Uso de disco de {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Pantalla"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Uso de CPU de Docker" msgstr "Uso de CPU de Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Uso de memoria de Docker" msgstr "Uso de memoria de Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "E/S de red de Docker" msgstr "E/S de red de Docker"
@@ -688,7 +729,7 @@ msgstr "Efímero"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Error" msgid "Error"
msgstr "Error" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Example:" msgid "Example:"
@@ -731,7 +772,7 @@ msgstr "Exporta la configuración actual de sus sistemas."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Failed" msgid "Failed"
@@ -770,7 +811,7 @@ msgstr "Fallidos: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -783,7 +824,7 @@ msgstr "Huella dactilar"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Firmware" msgid "Firmware"
msgstr "Firmware" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "Comando FreeBSD" msgstr "Comando FreeBSD"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Llena" msgstr "Llena"
@@ -808,17 +850,21 @@ msgstr "Llena"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "General" msgid "General"
msgstr "General" msgstr ""
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Global" msgid "Global"
msgstr "Global" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "Motores GPU" msgstr "Motores GPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "Consumo de energía de la GPU" msgstr "Consumo de energía de la GPU"
@@ -826,6 +872,7 @@ msgstr "Consumo de energía de la GPU"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "Uso de GPU" msgstr "Uso de GPU"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Cuadrícula" msgstr "Cuadrícula"
@@ -836,7 +883,7 @@ msgstr "Estado"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr "Latido"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -864,6 +911,21 @@ msgstr "Método HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Método HTTP: POST, GET o HEAD (predeterminado: POST)" msgstr "Método HTTP: POST, GET o HEAD (predeterminado: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "Espera de E/S"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "Tiempo de E/S"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "Utilización de E/S"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -912,7 +974,7 @@ msgstr "Ciclo de vida"
msgid "limit" msgid "limit"
msgstr "límite" msgstr "límite"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Carga media" msgstr "Carga media"
@@ -941,6 +1003,7 @@ msgstr "Estado de carga"
msgid "Loading..." msgid "Loading..."
msgstr "Cargando..." msgstr "Cargando..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Cerrar sesión" msgstr "Cerrar sesión"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Instrucciones manuales de configuración" msgstr "Instrucciones manuales de configuración"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Máx. 1 min" msgstr "Máx. 1 min"
@@ -999,15 +1062,16 @@ msgstr "Límite de memoria"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Pico de memoria" msgstr "Pico de memoria"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Uso de memoria" msgstr "Uso de memoria"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Uso de memoria de los contenedores Docker" msgstr "Uso de memoria de los contenedores Docker"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Modelo" msgstr "Modelo"
@@ -1025,11 +1089,11 @@ msgstr "Nombre"
msgid "Net" msgid "Net"
msgstr "Red" msgstr "Red"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Tráfico de red de los contenedores Docker" msgstr "Tráfico de red de los contenedores Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1045,7 +1109,7 @@ msgstr "Unidad de red"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "No" msgid "No"
msgstr "No" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
@@ -1171,6 +1235,10 @@ msgstr "Formato de carga útil (payload)"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Uso promedio por núcleo" msgstr "Uso promedio por núcleo"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Porcentaje de tiempo que el disco está ocupado con E/S"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Porcentaje de tiempo dedicado a cada estado" msgstr "Porcentaje de tiempo dedicado a cada estado"
@@ -1220,13 +1288,18 @@ msgstr "Por favor, inicia sesión en tu cuenta"
msgid "Port" msgid "Port"
msgstr "Puerto" msgstr "Puerto"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Puertos"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Encendido" msgstr "Encendido"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Utilización precisa en el momento registrado" msgstr "Utilización precisa en el momento registrado"
@@ -1243,17 +1316,24 @@ msgstr "Proceso iniciado"
msgid "Public Key" msgid "Public Key"
msgstr "Clave pública" msgstr "Clave pública"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Profundidad de cola"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Horas de silencio" msgstr "Horas de silencio"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Lectura" msgstr "Lectura"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Recibido" msgstr "Recibido"
@@ -1326,6 +1406,10 @@ msgstr "Detalles S.M.A.R.T."
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "Autoprueba S.M.A.R.T." msgstr "Autoprueba S.M.A.R.T."
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Guardar {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Guarda la dirección usando la tecla enter o coma. Deja en blanco para desactivar las notificaciones por correo." msgstr "Guarda la dirección usando la tecla enter o coma. Deja en blanco para desactivar las notificaciones por correo."
@@ -1335,10 +1419,6 @@ msgstr "Guarda la dirección usando la tecla enter o coma. Deja en blanco para d
msgid "Save Settings" msgid "Save Settings"
msgstr "Guardar configuración" msgstr "Guardar configuración"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Guardar sistema"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Guardado en la base de datos y no expira hasta que lo desactives." msgstr "Guardado en la base de datos y no expira hasta que lo desactives."
@@ -1387,7 +1467,7 @@ msgstr "Envíe pings salientes periódicos a un servicio de monitorización exte
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Enviar latido de prueba" msgstr "Enviar latido de prueba"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Enviado" msgstr "Enviado"
@@ -1399,6 +1479,7 @@ msgstr "Número de serie"
msgid "Service Details" msgid "Service Details"
msgstr "Detalles del servicio" msgstr "Detalles del servicio"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Servicios" msgstr "Servicios"
@@ -1414,8 +1495,10 @@ msgstr "Configure las siguientes variables de entorno en su hub Beszel para habi
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Configuración" msgstr "Configuración"
@@ -1459,17 +1542,18 @@ msgstr "Estado"
msgid "Sub State" msgid "Sub State"
msgstr "Subestado" msgstr "Subestado"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Espacio de swap utilizado por el sistema" msgstr "Espacio de swap utilizado por el sistema"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Uso de swap" msgstr "Uso de swap"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "Uso de swap"
msgid "System" msgid "System"
msgstr "Sistema" msgstr "Sistema"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Promedios de carga del sistema a lo largo del tiempo" msgstr "Promedios de carga del sistema a lo largo del tiempo"
@@ -1501,6 +1585,11 @@ msgstr "Los sistemas pueden ser gestionados en un archivo <0>config.yml</0> dent
msgid "Table" msgid "Table"
msgstr "Tabla" msgstr "Tabla"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Pestañas"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Tareas" msgstr "Tareas"
@@ -1511,7 +1600,7 @@ msgstr "Tareas"
msgid "Temp" msgid "Temp"
msgstr "Temperatura" msgstr "Temperatura"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperatura" msgstr "Temperatura"
@@ -1520,7 +1609,7 @@ msgstr "Temperatura"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Unidad de temperatura" msgstr "Unidad de temperatura"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Temperaturas de los sensores del sistema" msgstr "Temperaturas de los sensores del sistema"
@@ -1552,11 +1641,11 @@ msgstr "Esta acción no se puede deshacer. Esto eliminará permanentemente todos
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Esto eliminará permanentemente todos los registros seleccionados de la base de datos." msgstr "Esto eliminará permanentemente todos los registros seleccionados de la base de datos."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Rendimiento de {extraFsName}" msgstr "Rendimiento de {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Rendimiento del sistema de archivos raíz" msgstr "Rendimiento del sistema de archivos raíz"
@@ -1568,11 +1657,6 @@ msgstr "Formato de hora"
msgid "To email(s)" msgid "To email(s)"
msgstr "A correo(s)" msgstr "A correo(s)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Alternar cuadrícula"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1581,7 +1665,7 @@ msgstr "Alternar tema"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1600,7 +1684,7 @@ msgstr "Los tokens y las huellas digitales se utilizan para autenticar las conex
#: src/components/ui/chart.tsx #: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx #: src/components/ui/chart.tsx
msgid "Total" msgid "Total"
msgstr "Total" msgstr ""
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
@@ -1610,10 +1694,15 @@ msgstr "Datos totales recibidos por cada interfaz"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Datos totales enviados por cada interfaz" msgstr "Datos totales enviados por cada interfaz"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
msgstr "Total: {0}" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Triggered by" msgid "Triggered by"
@@ -1728,22 +1817,22 @@ msgstr "Cargar"
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime" msgid "Uptime"
msgstr "Uptime" msgstr "Tiempo de actividad"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Uso" msgstr "Uso"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Uso de la partición raíz" msgstr "Uso de la partición raíz"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Usado" msgstr "Usado"
@@ -1752,6 +1841,11 @@ msgstr "Usado"
msgid "Users" msgid "Users"
msgstr "Usuarios" msgstr "Usuarios"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utilización"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Valor" msgstr "Valor"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Vista" msgstr "Vista"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Ver más" msgstr "Ver más"
@@ -1773,7 +1868,7 @@ msgstr "Ver tus 200 alertas más recientes."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Columnas visibles" msgstr "Columnas visibles"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Esperando suficientes registros para mostrar" msgstr "Esperando suficientes registros para mostrar"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Comando Windows" msgstr "Comando Windows"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Escritura" msgstr "Escritura"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n" "Language: fa\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} در دسترس است"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "۱ ساعت" msgstr "۱ ساعت"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "۱ دقیقه" msgstr "۱ دقیقه"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "۱۲ ساعت" msgstr "۱۲ ساعت"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "۱۵ دقیقه" msgstr "۱۵ دقیقه"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "۳۰ روز" msgstr "۳۰ روز"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "۵ دقیقه" msgstr "۵ دقیقه"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "وضعیت فعال" msgstr "وضعیت فعال"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "افزودن {foo}" msgstr "افزودن {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "افزودن <0>سیستم</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "افزودن سیستم"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "افزودن آدرس اینترنتی" msgstr "افزودن آدرس اینترنتی"
@@ -134,6 +135,7 @@ msgstr "تنظیم عرض چیدمان اصلی"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "مدیر" msgstr "مدیر"
@@ -163,6 +165,7 @@ msgstr "هشدارها"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "همه کانتینرها" msgstr "همه کانتینرها"
@@ -188,11 +191,11 @@ msgstr "آیا مطمئن هستید؟"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "کپی خودکار نیاز به یک زمینه امن دارد." msgstr "کپی خودکار نیاز به یک زمینه امن دارد."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "میانگین" msgstr "میانگین"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "میانگین استفاده از CPU کانتینرها" msgstr "میانگین استفاده از CPU کانتینرها"
@@ -206,20 +209,29 @@ msgstr "میانگین به زیر <0>{value}{0}</0> می‌افتد"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "میانگین از <0>{value}{0}</0> فراتر رفته است" msgstr "میانگین از <0>{value}{0}</0> فراتر رفته است"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "میانگین تعداد عملیات ورودی/خروجی در انتظار سرویس"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "میانگین مصرف برق پردازنده‌های گرافیکی" msgstr "میانگین مصرف برق پردازنده‌های گرافیکی"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "میانگین زمان صف تا تکمیل به ازای هر عملیات"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "میانگین استفاده از CPU در کل سیستم" msgstr "میانگین استفاده از CPU در کل سیستم"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "میانگین استفاده از {0}" msgstr "میانگین استفاده از {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "میانگین استفاده از موتورهای GPU" msgstr "میانگین استفاده از موتورهای GPU"
@@ -228,7 +240,7 @@ msgstr "میانگین استفاده از موتورهای GPU"
msgid "Backups" msgid "Backups"
msgstr "پشتیبان‌گیری‌ها" msgstr "پشتیبان‌گیری‌ها"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "پهنای باند" msgstr "پهنای باند"
@@ -238,7 +250,7 @@ msgstr "پهنای باند"
msgid "Bat" msgid "Bat"
msgstr "باتری" msgstr "باتری"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "باتری" msgstr "باتری"
@@ -288,7 +300,7 @@ msgstr "وضعیت بوت"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "بایت (کیلوبایت بر ثانیه، مگابایت بر ثانیه، گیگابایت بر ثانیه)" msgstr "بایت (کیلوبایت بر ثانیه، مگابایت بر ثانیه، گیگابایت بر ثانیه)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "حافظه پنهان / بافرها" msgstr "حافظه پنهان / بافرها"
@@ -334,7 +346,7 @@ msgstr "تغییر واحدهای نمایش برای معیارها."
msgid "Change general application options." msgid "Change general application options."
msgstr "تغییر گزینه‌های کلی برنامه." msgstr "تغییر گزینه‌های کلی برنامه."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "شارژ" msgstr "شارژ"
@@ -347,6 +359,10 @@ msgstr "در حال شارژ"
msgid "Chart options" msgid "Chart options"
msgstr "گزینه‌های نمودار" msgstr "گزینه‌های نمودار"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "عرض نمودار"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "ایمیل {email} خود را برای لینک بازنشانی بررسی کنید." msgstr "ایمیل {email} خود را برای لینک بازنشانی بررسی کنید."
@@ -407,6 +423,10 @@ msgstr "تعارض‌ها"
msgid "Connection is down" msgid "Connection is down"
msgstr "اتصال قطع است" msgstr "اتصال قطع است"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "کانتینرها"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "کپی متغیرهای محیط" msgstr "کپی متغیرهای محیط"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "کپی از"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "کپی میزبان" msgstr "کپی میزبان"
@@ -462,6 +487,11 @@ msgstr "محتوای <0>docker-compose.yml</0> عامل زیر را کپی کن
msgid "Copy YAML" msgid "Copy YAML"
msgstr "کپی YAML" msgstr "کپی YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "هسته"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -484,8 +514,8 @@ msgstr "زمان CPU"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "تجزیه زمان CPU" msgstr "تجزیه زمان CPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "آپلود تجمعی" msgstr "آپلود تجمعی"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "وضعیت فعلی" msgstr "وضعیت فعلی"
@@ -531,6 +561,11 @@ msgstr "چرخه‌ها"
msgid "Daily" msgid "Daily"
msgstr "روزانه" msgstr "روزانه"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "پیش‌فرض"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "بازه زمانی پیش‌فرض" msgstr "بازه زمانی پیش‌فرض"
@@ -563,11 +598,12 @@ msgstr "دستگاه"
msgid "Discharging" msgid "Discharging"
msgstr "در حال تخلیه" msgstr "در حال تخلیه"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "دیسک" msgstr "دیسک"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "ورودی/خروجی دیسک" msgstr "ورودی/خروجی دیسک"
@@ -575,25 +611,30 @@ msgstr "ورودی/خروجی دیسک"
msgid "Disk unit" msgid "Disk unit"
msgstr "واحد دیسک" msgstr "واحد دیسک"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "میزان استفاده از دیسک" msgstr "میزان استفاده از دیسک"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "میزان استفاده از دیسک {extraFsName}" msgstr "میزان استفاده از دیسک {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "نمایش"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "میزان استفاده از CPU داکر" msgstr "میزان استفاده از CPU داکر"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "میزان استفاده از حافظه داکر" msgstr "میزان استفاده از حافظه داکر"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "ورودی/خروجی شبکه داکر" msgstr "ورودی/خروجی شبکه داکر"
@@ -770,7 +811,7 @@ msgstr "ناموفق: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "دستور FreeBSD" msgstr "دستور FreeBSD"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "پر" msgstr "پر"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "جهانی" msgstr "جهانی"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr "پردازنده گرافیکی"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "موتورهای GPU" msgstr "موتورهای GPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "مصرف برق پردازنده گرافیکی" msgstr "مصرف برق پردازنده گرافیکی"
@@ -826,6 +872,7 @@ msgstr "مصرف برق پردازنده گرافیکی"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "میزان استفاده از GPU" msgstr "میزان استفاده از GPU"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "جدول" msgstr "جدول"
@@ -864,6 +911,21 @@ msgstr "متد HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "متد HTTP: POST، GET، یا HEAD (پیش‌فرض: POST)" msgstr "متد HTTP: POST، GET، یا HEAD (پیش‌فرض: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "انتظار ورودی/خروجی"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "زمان ورودی/خروجی"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "استفاده از ورودی/خروجی"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -912,7 +974,7 @@ msgstr "چرخه حیات"
msgid "limit" msgid "limit"
msgstr "محدودیت" msgstr "محدودیت"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "میانگین بار" msgstr "میانگین بار"
@@ -941,6 +1003,7 @@ msgstr "وضعیت بارگذاری"
msgid "Loading..." msgid "Loading..."
msgstr "در حال بارگذاری..." msgstr "در حال بارگذاری..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "خروج" msgstr "خروج"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "دستورالعمل‌های راه‌اندازی دستی" msgstr "دستورالعمل‌های راه‌اندازی دستی"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "حداکثر ۱ دقیقه" msgstr "حداکثر ۱ دقیقه"
@@ -999,15 +1062,16 @@ msgstr "محدودیت حافظه"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "حداکثر حافظه" msgstr "حداکثر حافظه"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "میزان استفاده از حافظه" msgstr "میزان استفاده از حافظه"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "میزان استفاده از حافظه کانتینرهای داکر" msgstr "میزان استفاده از حافظه کانتینرهای داکر"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "مدل" msgstr "مدل"
@@ -1025,11 +1089,11 @@ msgstr "نام"
msgid "Net" msgid "Net"
msgstr "شبکه" msgstr "شبکه"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "ترافیک شبکه کانتینرهای داکر" msgstr "ترافیک شبکه کانتینرهای داکر"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "فرمت پی‌لود"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "میانگین استفاده در هر هسته" msgstr "میانگین استفاده در هر هسته"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "درصد زمانی که دیسک مشغول عملیات ورودی/خروجی است"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "درصد زمان صرف شده در هر حالت" msgstr "درصد زمان صرف شده در هر حالت"
@@ -1220,13 +1288,18 @@ msgstr "لطفاً به حساب کاربری خود وارد شوید"
msgid "Port" msgid "Port"
msgstr "پورت" msgstr "پورت"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "پورت‌ها"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "روشن کردن" msgstr "روشن کردن"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "میزان دقیق استفاده در زمان ثبت شده" msgstr "میزان دقیق استفاده در زمان ثبت شده"
@@ -1243,17 +1316,24 @@ msgstr "فرآیند شروع شد"
msgid "Public Key" msgid "Public Key"
msgstr "کلید عمومی" msgstr "کلید عمومی"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "عمق صف"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "ساعات آرام" msgstr "ساعات آرام"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "خواندن" msgstr "خواندن"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "دریافت شد" msgstr "دریافت شد"
@@ -1326,6 +1406,10 @@ msgstr "جزئیات S.M.A.R.T"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "تست خود S.M.A.R.T" msgstr "تست خود S.M.A.R.T"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "ذخیره {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "آدرس را با استفاده از کلید Enter یا کاما ذخیره کنید. برای غیرفعال کردن اعلان‌های ایمیلی، خالی بگذارید." msgstr "آدرس را با استفاده از کلید Enter یا کاما ذخیره کنید. برای غیرفعال کردن اعلان‌های ایمیلی، خالی بگذارید."
@@ -1335,10 +1419,6 @@ msgstr "آدرس را با استفاده از کلید Enter یا کاما ذخ
msgid "Save Settings" msgid "Save Settings"
msgstr "ذخیره تنظیمات" msgstr "ذخیره تنظیمات"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "ذخیره سیستم"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "در پایگاه داده ذخیره شده و تا زمانی که آن را غیرفعال نکنید، منقضی نمی‌شود." msgstr "در پایگاه داده ذخیره شده و تا زمانی که آن را غیرفعال نکنید، منقضی نمی‌شود."
@@ -1387,7 +1467,7 @@ msgstr "پینگ‌های خروجی دوره‌ای را به یک سرویس
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "ارسال ضربان قلب آزمایشی" msgstr "ارسال ضربان قلب آزمایشی"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "ارسال شد" msgstr "ارسال شد"
@@ -1399,6 +1479,7 @@ msgstr "شماره سریال"
msgid "Service Details" msgid "Service Details"
msgstr "جزئیات سرویس" msgstr "جزئیات سرویس"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "سرویس‌ها" msgstr "سرویس‌ها"
@@ -1414,8 +1495,10 @@ msgstr "متغیرهای محیطی زیر را در هاب Beszel خود تنظ
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "تنظیمات" msgstr "تنظیمات"
@@ -1459,17 +1542,18 @@ msgstr "وضعیت"
msgid "Sub State" msgid "Sub State"
msgstr "وضعیت فرعی" msgstr "وضعیت فرعی"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "فضای Swap استفاده شده توسط سیستم" msgstr "فضای Swap استفاده شده توسط سیستم"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "میزان استفاده از Swap" msgstr "میزان استفاده از Swap"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "میزان استفاده از Swap"
msgid "System" msgid "System"
msgstr "سیستم" msgstr "سیستم"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "میانگین بار سیستم در طول زمان" msgstr "میانگین بار سیستم در طول زمان"
@@ -1501,6 +1585,11 @@ msgstr "سیستم‌ها ممکن است در یک فایل <0>config.yml</0>
msgid "Table" msgid "Table"
msgstr "جدول" msgstr "جدول"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "تب‌ها"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "وظایف" msgstr "وظایف"
@@ -1511,7 +1600,7 @@ msgstr "وظایف"
msgid "Temp" msgid "Temp"
msgstr "دما" msgstr "دما"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "دما" msgstr "دما"
@@ -1520,7 +1609,7 @@ msgstr "دما"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "واحد دما" msgstr "واحد دما"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "دمای حسگرهای سیستم" msgstr "دمای حسگرهای سیستم"
@@ -1552,11 +1641,11 @@ msgstr "این عمل قابل برگشت نیست. این کار تمام رک
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "این کار تمام رکوردهای انتخاب شده را برای همیشه از پایگاه داده حذف خواهد کرد." msgstr "این کار تمام رکوردهای انتخاب شده را برای همیشه از پایگاه داده حذف خواهد کرد."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "توان عملیاتی {extraFsName}" msgstr "توان عملیاتی {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "توان عملیاتی سیستم فایل ریشه" msgstr "توان عملیاتی سیستم فایل ریشه"
@@ -1568,11 +1657,6 @@ msgstr "فرمت زمان"
msgid "To email(s)" msgid "To email(s)"
msgstr "به ایمیل(ها)" msgstr "به ایمیل(ها)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "تغییر نمایش جدول"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1610,6 +1694,11 @@ msgstr "داده‌های کل دریافت شده برای هر رابط"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "داده‌های کل ارسال شده برای هر رابط" msgstr "داده‌های کل ارسال شده برای هر رابط"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1730,20 +1819,20 @@ msgstr "آپلود"
msgid "Uptime" msgid "Uptime"
msgstr "آپتایم" msgstr "آپتایم"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "میزان استفاده" msgstr "میزان استفاده"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "میزان استفاده از پارتیشن ریشه" msgstr "میزان استفاده از پارتیشن ریشه"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "استفاده شده" msgstr "استفاده شده"
@@ -1752,6 +1841,11 @@ msgstr "استفاده شده"
msgid "Users" msgid "Users"
msgstr "کاربران" msgstr "کاربران"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "بهره‌وری"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "مقدار" msgstr "مقدار"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "مشاهده" msgstr "مشاهده"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "مشاهده بیشتر" msgstr "مشاهده بیشتر"
@@ -1773,7 +1868,7 @@ msgstr "۲۰۰ هشدار اخیر خود را مشاهده کنید."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "فیلدهای قابل مشاهده" msgstr "فیلدهای قابل مشاهده"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "در انتظار رکوردهای کافی برای نمایش" msgstr "در انتظار رکوردهای کافی برای نمایش"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "دستور Windows" msgstr "دستور Windows"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "نوشتن" msgstr "نوشتن"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n" "Language: fr\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -18,11 +18,17 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} disponible"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected." msgid "{0} of {1} row(s) selected."
msgstr "{0} sur {1} ligne(s) sélectionnée(s)." msgstr "{0} sur {1} ligne(s) sectionner."
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
msgid "{cores, plural, one {# core} other {# cores}}" msgid "{cores, plural, one {# core} other {# cores}}"
@@ -49,13 +55,13 @@ msgid "1 hour"
msgstr "1 heure" msgstr "1 heure"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 minute" msgid "1 minute"
msgstr "1 minute" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 week" msgid "1 week"
@@ -66,9 +72,9 @@ msgid "12 hours"
msgstr "12 heures" msgstr "12 heures"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 min" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "24 hours" msgid "24 hours"
@@ -79,9 +85,9 @@ msgid "30 days"
msgstr "30 jours" msgstr "30 jours"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 min" msgstr ""
#. Table column #. Table column
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -89,14 +95,14 @@ msgstr "5 min"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Actions" msgid "Actions"
msgstr "Actions" msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Active" msgid "Active"
msgstr "Active" msgstr ""
#: src/components/active-alerts.tsx #: src/components/active-alerts.tsx
msgid "Active Alerts" msgid "Active Alerts"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "État actif" msgstr "État actif"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Ajouter {foo}" msgstr "Ajouter {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Ajouter <0>un Système</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Ajouter un système"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Ajouter lURL" msgstr "Ajouter lURL"
@@ -134,8 +135,9 @@ msgstr "Ajuster la largeur de la mise en page principale"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "After" msgid "After"
@@ -147,7 +149,7 @@ msgstr "Après avoir défini les variables d'environnement, redémarrez votre hu
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Agent" msgid "Agent"
msgstr "Agent" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -163,6 +165,7 @@ msgstr "Alertes"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Tous les conteneurs" msgstr "Tous les conteneurs"
@@ -188,11 +191,11 @@ msgstr "Êtes-vous sûr ?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "La copie automatique nécessite un contexte sécurisé." msgstr "La copie automatique nécessite un contexte sécurisé."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Moyenne" msgstr "Moyenne"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Utilisation moyenne du CPU des conteneurs" msgstr "Utilisation moyenne du CPU des conteneurs"
@@ -206,20 +209,29 @@ msgstr "La moyenne descend en dessous de <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "La moyenne dépasse <0>{value}{0}</0>" msgstr "La moyenne dépasse <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Nombre moyen d'opérations d'E/S en attente d'être traitées"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Consommation d'énergie moyenne des GPUs" msgstr "Consommation d'énergie moyenne des GPUs"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Temps moyen de file d'attente jusqu'à l'achèvement par opération"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Utilisation moyenne du CPU à l'échelle du système" msgstr "Utilisation moyenne du CPU à l'échelle du système"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Utilisation moyenne de {0}" msgstr "Utilisation moyenne de {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Utilisation moyenne des moteurs GPU" msgstr "Utilisation moyenne des moteurs GPU"
@@ -228,7 +240,7 @@ msgstr "Utilisation moyenne des moteurs GPU"
msgid "Backups" msgid "Backups"
msgstr "Sauvegardes" msgstr "Sauvegardes"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Bande passante" msgstr "Bande passante"
@@ -236,9 +248,9 @@ msgstr "Bande passante"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Batterie" msgstr "Batterie"
@@ -277,7 +289,7 @@ msgstr "Binaire"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)" msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bits (Kbps, Mbps, Gbps)" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Boot state" msgid "Boot state"
@@ -286,9 +298,9 @@ msgstr "État de démarrage"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)" msgstr ""
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / Tampons" msgstr "Cache / Tampons"
@@ -324,7 +336,7 @@ msgstr "Attention - perte de données potentielle"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -334,9 +346,9 @@ msgstr "Ajuster les unités d'affichage pour les métriques."
msgid "Change general application options." msgid "Change general application options."
msgstr "Modifier les options générales de l'application." msgstr "Modifier les options générales de l'application."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Charge" msgstr ""
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
@@ -347,6 +359,10 @@ msgstr "En charge"
msgid "Chart options" msgid "Chart options"
msgstr "Options de graphique" msgstr "Options de graphique"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Largeur du graphique"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Vérifiez {email} pour un lien de réinitialisation." msgstr "Vérifiez {email} pour un lien de réinitialisation."
@@ -407,6 +423,10 @@ msgstr "Conflits"
msgid "Connection is down" msgid "Connection is down"
msgstr "Connexion interrompue" msgstr "Connexion interrompue"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Conteneurs"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Copier env" msgstr "Copier env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Copier depuis"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Copier l'hôte" msgstr "Copier l'hôte"
@@ -462,11 +487,16 @@ msgstr "Copiez le contenu du<0>docker-compose.yml</0> pour l'agent ci-dessous, o
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Copier YAML" msgstr "Copier YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Cœur"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -484,8 +514,8 @@ msgstr "Temps CPU"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "Répartition du temps CPU" msgstr "Répartition du temps CPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,20 +547,25 @@ msgid "Cumulative Upload"
msgstr "Téléversement cumulatif" msgstr "Téléversement cumulatif"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "État actuel" msgstr "État actuel"
#. Power Cycles #. Power Cycles
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Cycles" msgid "Cycles"
msgstr "Cycles" msgstr ""
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Daily" msgid "Daily"
msgstr "Quotidien" msgstr "Quotidien"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Par défaut"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Période par défaut" msgstr "Période par défaut"
@@ -548,7 +583,7 @@ msgstr "Supprimer l'empreinte"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Description" msgid "Description"
msgstr "Description" msgstr ""
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
msgid "Detail" msgid "Detail"
@@ -563,11 +598,12 @@ msgstr "Appareil"
msgid "Discharging" msgid "Discharging"
msgstr "En décharge" msgstr "En décharge"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disque" msgstr "Disque"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Entrée/Sortie disque" msgstr "Entrée/Sortie disque"
@@ -575,32 +611,37 @@ msgstr "Entrée/Sortie disque"
msgid "Disk unit" msgid "Disk unit"
msgstr "Unité disque" msgstr "Unité disque"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Utilisation du disque" msgstr "Utilisation du disque"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Utilisation du disque de {extraFsName}" msgstr "Utilisation du disque de {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Affichage"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Utilisation du CPU Docker" msgstr "Utilisation du CPU Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Utilisation de la mémoire Docker" msgstr "Utilisation de la mémoire Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Entrée/Sortie réseau Docker" msgstr "Entrée/Sortie réseau Docker"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Documentation" msgid "Documentation"
msgstr "Documentation" msgstr ""
#. Context: System is down #. Context: System is down
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
@@ -636,7 +677,7 @@ msgstr "Modifier {foo}"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx #: src/components/login/otp-forms.tsx
msgid "Email" msgid "Email"
msgstr "Email" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Email notifications" msgid "Email notifications"
@@ -731,7 +772,7 @@ msgstr "Exportez la configuration actuelle de vos systèmes."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Failed" msgid "Failed"
@@ -770,7 +811,7 @@ msgstr "Échec : {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "Commande FreeBSD" msgstr "Commande FreeBSD"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Pleine" msgstr "Pleine"
@@ -812,13 +854,17 @@ msgstr "Général"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Global" msgid "Global"
msgstr "Global" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "Moteurs GPU" msgstr "Moteurs GPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "Consommation du GPU" msgstr "Consommation du GPU"
@@ -826,6 +872,7 @@ msgstr "Consommation du GPU"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "Utilisation GPU" msgstr "Utilisation GPU"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Grille" msgstr "Grille"
@@ -836,7 +883,7 @@ msgstr "Santé"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr "Pulsation"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -864,6 +911,21 @@ msgstr "Méthode HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Méthode HTTP : POST, GET ou HEAD (par défaut : POST)" msgstr "Méthode HTTP : POST, GET ou HEAD (par défaut : POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "Attente E/S"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "Temps E/S"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "Utilisation E/S"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -876,7 +938,7 @@ msgstr "Si vous avez perdu le mot de passe de votre compte administrateur, vous
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
msgctxt "Docker image" msgctxt "Docker image"
msgid "Image" msgid "Image"
msgstr "Image" msgstr ""
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive" msgid "Inactive"
@@ -912,7 +974,7 @@ msgstr "Cycle de vie"
msgid "limit" msgid "limit"
msgstr "limite" msgstr "limite"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Charge moyenne" msgstr "Charge moyenne"
@@ -941,6 +1003,7 @@ msgstr "État de charge"
msgid "Loading..." msgid "Loading..."
msgstr "Chargement..." msgstr "Chargement..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Déconnexion" msgstr "Déconnexion"
@@ -978,9 +1041,9 @@ msgid "Manual setup instructions"
msgstr "Guide pour une installation manuelle" msgstr "Guide pour une installation manuelle"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Max 1 min" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
@@ -999,15 +1062,16 @@ msgstr "Limite mémoire"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Pic mémoire" msgstr "Pic mémoire"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Utilisation de la mémoire" msgstr "Utilisation de la mémoire"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Utilisation de la mémoire des conteneurs Docker" msgstr "Utilisation de la mémoire des conteneurs Docker"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Modèle" msgstr "Modèle"
@@ -1025,11 +1089,11 @@ msgstr "Nom"
msgid "Net" msgid "Net"
msgstr "Rés" msgstr "Rés"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Trafic réseau des conteneurs Docker" msgstr "Trafic réseau des conteneurs Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1074,7 +1138,7 @@ msgstr "Aucun système trouvé."
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Notifications" msgid "Notifications"
msgstr "Notifications" msgstr ""
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "OAuth 2 / OIDC support" msgid "OAuth 2 / OIDC support"
@@ -1117,7 +1181,7 @@ msgstr "Écraser les alertes existantes"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "Page" msgid "Page"
msgstr "Page" msgstr ""
#. placeholder {0}: table.getState().pagination.pageIndex + 1 #. placeholder {0}: table.getState().pagination.pageIndex + 1
#. placeholder {1}: table.getPageCount() #. placeholder {1}: table.getPageCount()
@@ -1152,7 +1216,7 @@ msgstr "Passé"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Pause" msgid "Pause"
msgstr "Pause" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Paused" msgid "Paused"
@@ -1171,13 +1235,17 @@ msgstr "Format de la charge utile"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Utilisation moyenne par cœur" msgstr "Utilisation moyenne par cœur"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Pourcentage de temps pendant lequel le disque est occupé par des E/S"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Pourcentage de temps passé dans chaque état" msgstr "Pourcentage de temps passé dans chaque état"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent" msgid "Permanent"
msgstr "Permanent" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence" msgid "Persistence"
@@ -1218,15 +1286,20 @@ msgstr "Veuillez vous connecter à votre compte"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
msgstr "Port" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Allumage" msgstr "Allumage"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Utilisation précise au moment enregistré" msgstr "Utilisation précise au moment enregistré"
@@ -1243,17 +1316,24 @@ msgstr "Processus démarré"
msgid "Public Key" msgid "Public Key"
msgstr "Clé publique" msgstr "Clé publique"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Profondeur de file d'attente"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Heures calmes" msgstr "Heures calmes"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Lecture" msgstr "Lecture"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Reçu" msgstr "Reçu"
@@ -1326,6 +1406,10 @@ msgstr "Détails S.M.A.R.T."
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "Auto-test S.M.A.R.T." msgstr "Auto-test S.M.A.R.T."
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Enregistrer {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Enregistrez l'adresse en utilisant la touche Entrée ou la virgule. Laissez vide pour désactiver les notifications par email." msgstr "Enregistrez l'adresse en utilisant la touche Entrée ou la virgule. Laissez vide pour désactiver les notifications par email."
@@ -1335,10 +1419,6 @@ msgstr "Enregistrez l'adresse en utilisant la touche Entrée ou la virgule. Lais
msgid "Save Settings" msgid "Save Settings"
msgstr "Enregistrer les paramètres" msgstr "Enregistrer les paramètres"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Sauvegarder le système"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Enregistré dans la base de données et n'expire pas tant que vous ne le désactivez pas." msgstr "Enregistré dans la base de données et n'expire pas tant que vous ne le désactivez pas."
@@ -1387,7 +1467,7 @@ msgstr "Envoyez des pings sortants périodiques vers un service de surveillance
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Envoyer un heartbeat de test" msgstr "Envoyer un heartbeat de test"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Envoyé" msgstr "Envoyé"
@@ -1399,9 +1479,10 @@ msgstr "Numéro de série"
msgid "Service Details" msgid "Service Details"
msgstr "Détails du service" msgstr "Détails du service"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Services" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors." msgid "Set percentage thresholds for meter colors."
@@ -1414,8 +1495,10 @@ msgstr "Définissez les variables d'environnement suivantes sur votre hub Beszel
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Paramètres" msgstr "Paramètres"
@@ -1459,17 +1542,18 @@ msgstr "Statut"
msgid "Sub State" msgid "Sub State"
msgstr "Sous-état" msgstr "Sous-état"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Espace Swap utilisé par le système" msgstr "Espace Swap utilisé par le système"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Utilisation du swap" msgstr "Utilisation du swap"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "Utilisation du swap"
msgid "System" msgid "System"
msgstr "Système" msgstr "Système"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Charges moyennes du système dans le temps" msgstr "Charges moyennes du système dans le temps"
@@ -1501,6 +1585,11 @@ msgstr "Les systèmes peuvent être gérés dans un fichier <0>config.yml</0> à
msgid "Table" msgid "Table"
msgstr "Tableau" msgstr "Tableau"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Onglets"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Tâches" msgstr "Tâches"
@@ -1511,7 +1600,7 @@ msgstr "Tâches"
msgid "Temp" msgid "Temp"
msgstr "Temp." msgstr "Temp."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Température" msgstr "Température"
@@ -1520,7 +1609,7 @@ msgstr "Température"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Unité de température" msgstr "Unité de température"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Températures des capteurs du système" msgstr "Températures des capteurs du système"
@@ -1552,11 +1641,11 @@ msgstr "Cette action ne peut pas être annulée. Cela supprimera définitivement
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Ceci supprimera définitivement tous les enregistrements sélectionnés de la base de données." msgstr "Ceci supprimera définitivement tous les enregistrements sélectionnés de la base de données."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Débit de {extraFsName}" msgstr "Débit de {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Débit du système de fichiers racine" msgstr "Débit du système de fichiers racine"
@@ -1568,11 +1657,6 @@ msgstr "Format d'heure"
msgid "To email(s)" msgid "To email(s)"
msgstr "Aux email(s)" msgstr "Aux email(s)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Basculer la grille"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1581,7 +1665,7 @@ msgstr "Changer le thème"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1600,7 +1684,7 @@ msgstr "Les tokens et les empreintes sont utilisés pour authentifier les connex
#: src/components/ui/chart.tsx #: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx #: src/components/ui/chart.tsx
msgid "Total" msgid "Total"
msgstr "Total" msgstr ""
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
@@ -1610,6 +1694,11 @@ msgstr "Données totales reçues pour chaque interface"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Données totales envoyées pour chaque interface" msgstr "Données totales envoyées pour chaque interface"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1671,7 +1760,7 @@ msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Type" msgid "Type"
msgstr "Type" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Unit file" msgid "Unit file"
@@ -1728,22 +1817,22 @@ msgstr "Téléverser"
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime" msgid "Uptime"
msgstr "Uptime" msgstr "Temps de fonctionnement"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Utilisation" msgstr "Utilisation"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Utilisation de la partition racine" msgstr "Utilisation de la partition racine"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Utilisé" msgstr "Utilisé"
@@ -1752,6 +1841,11 @@ msgstr "Utilisé"
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utilisation"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Valeur" msgstr "Valeur"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Vue" msgstr "Vue"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Voir plus" msgstr "Voir plus"
@@ -1773,7 +1868,7 @@ msgstr "Voir vos 200 dernières alertes."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Colonnes visibles" msgstr "Colonnes visibles"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "En attente de suffisamment d'enregistrements à afficher" msgstr "En attente de suffisamment d'enregistrements à afficher"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Commande Windows" msgstr "Commande Windows"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Écriture" msgstr "Écriture"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: he\n" "Language: he\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hebrew\n" "Language-Team: Hebrew\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" "Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} זמין"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "שעה" msgstr "שעה"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "דקה אחת" msgstr "דקה אחת"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 שעות" msgstr "12 שעות"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 דק'" msgstr "15 דק'"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 ימים" msgstr "30 ימים"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 דק'" msgstr "5 דק'"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "מצב פעיל" msgstr "מצב פעיל"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "הוסף {foo}" msgstr "הוסף {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "הוסף <0>מערכת</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "הוסף מערכת"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "הוסף URL" msgstr "הוסף URL"
@@ -134,6 +135,7 @@ msgstr "התאם את רוחב הפריסה הראשית"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "מנהל" msgstr "מנהל"
@@ -163,6 +165,7 @@ msgstr "התראות"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "כל הקונטיינרים" msgstr "כל הקונטיינרים"
@@ -188,11 +191,11 @@ msgstr "האם אתה בטוח?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "העתקה אוטומטית דורשת הקשר מאובטח." msgstr "העתקה אוטומטית דורשת הקשר מאובטח."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "ממוצע" msgstr "ממוצע"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "ניצול ממוצע של CPU בקונטיינרים" msgstr "ניצול ממוצע של CPU בקונטיינרים"
@@ -206,20 +209,29 @@ msgstr "הממוצע יורד מתחת ל-<0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "הממוצע עולה על <0>{value}{0}</0>" msgstr "הממוצע עולה על <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "מספר ממוצע של פעולות קלט/פלט הממתינות לטיפול"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "צריכת חשמל ממוצעת של GPUs" msgstr "צריכת חשמל ממוצעת של GPUs"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "זמן ממוצע מהתור ועד להשלמה לכל פעולה"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "ניצול ממוצע כלל-מערכתי של CPU" msgstr "ניצול ממוצע כלל-מערכתי של CPU"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "ניצול ממוצע של {0}" msgstr "ניצול ממוצע של {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "ניצול ממוצע של מנועי GPU" msgstr "ניצול ממוצע של מנועי GPU"
@@ -228,7 +240,7 @@ msgstr "ניצול ממוצע של מנועי GPU"
msgid "Backups" msgid "Backups"
msgstr "גיבויים" msgstr "גיבויים"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "רוחב פס" msgstr "רוחב פס"
@@ -238,7 +250,7 @@ msgstr "רוחב פס"
msgid "Bat" msgid "Bat"
msgstr "סוללה" msgstr "סוללה"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "סוללה" msgstr "סוללה"
@@ -288,7 +300,7 @@ msgstr "מצב אתחול"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "בתים (KB/s, MB/s, GB/s)" msgstr "בתים (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "מטמון / חוצצים" msgstr "מטמון / חוצצים"
@@ -334,7 +346,7 @@ msgstr "שנה יחידות תצוגה עבור מדדים."
msgid "Change general application options." msgid "Change general application options."
msgstr "שנה אפשרויות כלליות של היישום." msgstr "שנה אפשרויות כלליות של היישום."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "טעינה" msgstr "טעינה"
@@ -347,6 +359,10 @@ msgstr "בטעינה"
msgid "Chart options" msgid "Chart options"
msgstr "אפשרויות גרף" msgstr "אפשרויות גרף"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "רוחב תרשים"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "בדוק את {email} לקישור איפוס." msgstr "בדוק את {email} לקישור איפוס."
@@ -407,6 +423,10 @@ msgstr "התנגשויות"
msgid "Connection is down" msgid "Connection is down"
msgstr "החיבור נפל" msgstr "החיבור נפל"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "קונטיינרים"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "העתק env" msgstr "העתק env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "העתק מ-"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "העתק מארח" msgstr "העתק מארח"
@@ -462,11 +487,16 @@ msgstr "העתק את תוכן ה-<0>docker-compose.yml</0> עבור הסוכן
msgid "Copy YAML" msgid "Copy YAML"
msgstr "העתק YAML" msgstr "העתק YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "ליבה"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -484,8 +514,8 @@ msgstr "זמן CPU"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "פירוט זמן CPU" msgstr "פירוט זמן CPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "העלאה מצטברת" msgstr "העלאה מצטברת"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "מצב נוכחי" msgstr "מצב נוכחי"
@@ -531,6 +561,11 @@ msgstr "מחזורים"
msgid "Daily" msgid "Daily"
msgstr "יומי" msgstr "יומי"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "ברירת מחדל"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "תקופת זמן ברירת מחדל" msgstr "תקופת זמן ברירת מחדל"
@@ -563,11 +598,12 @@ msgstr "התקן"
msgid "Discharging" msgid "Discharging"
msgstr "בפריקה" msgstr "בפריקה"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "דיסק" msgstr "דיסק"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "דיסק I/O" msgstr "דיסק I/O"
@@ -575,25 +611,30 @@ msgstr "דיסק I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "יחידת דיסק" msgstr "יחידת דיסק"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "שימוש בדיסק" msgstr "שימוש בדיסק"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "שימוש בדיסק של {extraFsName}" msgstr "שימוש בדיסק של {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "תצוגה"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "שימוש CPU של Docker" msgstr "שימוש CPU של Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "שימוש זיכרון של Docker" msgstr "שימוש זיכרון של Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "I/O של רשת Docker" msgstr "I/O של רשת Docker"
@@ -770,7 +811,7 @@ msgstr "נכשל: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "פקודת FreeBSD" msgstr "פקודת FreeBSD"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "מלא" msgstr "מלא"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "גלובלי" msgstr "גלובלי"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr "מעבד גרפי"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "מנועי GPU" msgstr "מנועי GPU"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "צריכת חשמל GPU" msgstr "צריכת חשמל GPU"
@@ -826,6 +872,7 @@ msgstr "צריכת חשמל GPU"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "שימוש GPU" msgstr "שימוש GPU"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "רשת" msgstr "רשת"
@@ -864,6 +911,21 @@ msgstr "שיטת HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "שיטת HTTP: POST, GET, או HEAD (ברירת מחדל: POST)" msgstr "שיטת HTTP: POST, GET, או HEAD (ברירת מחדל: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "המתנת קלט/פלט"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "זמן קלט/פלט"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "ניצול קלט/פלט"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -912,7 +974,7 @@ msgstr "מחזור חיים"
msgid "limit" msgid "limit"
msgstr "גבול" msgstr "גבול"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "ממוצע עומס" msgstr "ממוצע עומס"
@@ -941,6 +1003,7 @@ msgstr "מצב עומס"
msgid "Loading..." msgid "Loading..."
msgstr "טוען..." msgstr "טוען..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "התנתק" msgstr "התנתק"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "הוראות התקנה ידניות" msgstr "הוראות התקנה ידניות"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "מקס 1 דק'" msgstr "מקס 1 דק'"
@@ -999,15 +1062,16 @@ msgstr "גבול זיכרון"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "שיא זיכרון" msgstr "שיא זיכרון"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "שימוש בזיכרון" msgstr "שימוש בזיכרון"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "שימוש בזיכרון של קונטיינרים של Docker" msgstr "שימוש בזיכרון של קונטיינרים של Docker"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "דגם" msgstr "דגם"
@@ -1025,11 +1089,11 @@ msgstr "שם"
msgid "Net" msgid "Net"
msgstr "רשת" msgstr "רשת"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "תעבורת רשת של קונטיינרים של Docker" msgstr "תעבורת רשת של קונטיינרים של Docker"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "פורמט מטען (Payload)"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "ניצול ממוצע לליבה" msgstr "ניצול ממוצע לליבה"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "אחוז הזמן שבו הדיסק עסוק בפעולות קלט/פלט"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "אחוז הזמן המוקדש לכל מצב" msgstr "אחוז הזמן המוקדש לכל מצב"
@@ -1220,13 +1288,18 @@ msgstr "אנא התחבר לחשבון שלך"
msgid "Port" msgid "Port"
msgstr "פורט" msgstr "פורט"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "פורטים"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "הפעלה" msgstr "הפעלה"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "ניצול מדויק בזמן הרשום" msgstr "ניצול מדויק בזמן הרשום"
@@ -1243,17 +1316,24 @@ msgstr "תהליך התחיל"
msgid "Public Key" msgid "Public Key"
msgstr "מפתח ציבורי" msgstr "מפתח ציבורי"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "עומק תור"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "שעות שקט" msgstr "שעות שקט"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "קריאה" msgstr "קריאה"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "התקבל" msgstr "התקבל"
@@ -1326,6 +1406,10 @@ msgstr "פרטי S.M.A.R.T."
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "בדיקה עצמית S.M.A.R.T." msgstr "בדיקה עצמית S.M.A.R.T."
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "שמור {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "שמור כתובת באמצעות מקש enter או פסיק. השאר ריק כדי להשבית התראות אימייל." msgstr "שמור כתובת באמצעות מקש enter או פסיק. השאר ריק כדי להשבית התראות אימייל."
@@ -1335,10 +1419,6 @@ msgstr "שמור כתובת באמצעות מקש enter או פסיק. השאר
msgid "Save Settings" msgid "Save Settings"
msgstr "שמור הגדרות" msgstr "שמור הגדרות"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "שמור מערכת"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "נשמר במסד הנתונים ולא פג תוקף עד שתבטל אותו." msgstr "נשמר במסד הנתונים ולא פג תוקף עד שתבטל אותו."
@@ -1387,7 +1467,7 @@ msgstr "שלח פינגים יוצאים תקופתיים לשירות ניטו
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "שלח פעימת לב לבדיקה" msgstr "שלח פעימת לב לבדיקה"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "נשלח" msgstr "נשלח"
@@ -1399,6 +1479,7 @@ msgstr "מספר סידורי"
msgid "Service Details" msgid "Service Details"
msgstr "פרטי שירות" msgstr "פרטי שירות"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "שירותים" msgstr "שירותים"
@@ -1414,8 +1495,10 @@ msgstr "הגדר את משתני הסביבה הבאים ב-Beszel hub שלך כ
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "הגדרות" msgstr "הגדרות"
@@ -1459,17 +1542,18 @@ msgstr "סטטוס"
msgid "Sub State" msgid "Sub State"
msgstr "מצב משני" msgstr "מצב משני"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "שטח swap בשימוש על ידי המערכת" msgstr "שטח swap בשימוש על ידי המערכת"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "שימוש ב-Swap" msgstr "שימוש ב-Swap"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "שימוש ב-Swap"
msgid "System" msgid "System"
msgstr "מערכת" msgstr "מערכת"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "ממוצעי עומס מערכת לאורך זמן" msgstr "ממוצעי עומס מערכת לאורך זמן"
@@ -1501,6 +1585,11 @@ msgstr "מערכות יכולות להיות מנוהלות בקובץ <0>config
msgid "Table" msgid "Table"
msgstr "טבלה" msgstr "טבלה"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "לשוניות"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "משימות" msgstr "משימות"
@@ -1511,7 +1600,7 @@ msgstr "משימות"
msgid "Temp" msgid "Temp"
msgstr "טמפ'" msgstr "טמפ'"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "טמפרטורה" msgstr "טמפרטורה"
@@ -1520,7 +1609,7 @@ msgstr "טמפרטורה"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "יחידת טמפרטורה" msgstr "יחידת טמפרטורה"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "טמפרטורות של חיישני המערכת" msgstr "טמפרטורות של חיישני המערכת"
@@ -1552,11 +1641,11 @@ msgstr "פעולה זו לא ניתנת לביטול. פעולה זו תמחק
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "פעולה זו תמחק לצמיתות את כל הרשומות שנבחרו ממסד הנתונים." msgstr "פעולה זו תמחק לצמיתות את כל הרשומות שנבחרו ממסד הנתונים."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "תפוקה של {extraFsName}" msgstr "תפוקה של {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "תפוקה של מערכת הקבצים הראשית" msgstr "תפוקה של מערכת הקבצים הראשית"
@@ -1568,11 +1657,6 @@ msgstr "פורמט זמן"
msgid "To email(s)" msgid "To email(s)"
msgstr "לאימייל(ים)" msgstr "לאימייל(ים)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "החלף רשת"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1581,7 +1665,7 @@ msgstr "החלף ערכת נושא"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1610,6 +1694,11 @@ msgstr "סך נתונים שהתקבלו עבור כל ממשק"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "סך נתונים שנשלחו עבור כל ממשק" msgstr "סך נתונים שנשלחו עבור כל ממשק"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1730,20 +1819,20 @@ msgstr "העלאה"
msgid "Uptime" msgid "Uptime"
msgstr "זמן פעילות" msgstr "זמן פעילות"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "שימוש" msgstr "שימוש"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "שימוש במחיצה הראשית" msgstr "שימוש במחיצה הראשית"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "בשימוש" msgstr "בשימוש"
@@ -1752,6 +1841,11 @@ msgstr "בשימוש"
msgid "Users" msgid "Users"
msgstr "משתמשים" msgstr "משתמשים"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "ניצולת"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "ערך" msgstr "ערך"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "צפה" msgstr "צפה"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "צפה בעוד" msgstr "צפה בעוד"
@@ -1773,7 +1868,7 @@ msgstr "צפה ב-200 ההתראות האחרונות שלך."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "שדות גלויים" msgstr "שדות גלויים"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "ממתין לרשומות מספיקות לתצוגה" msgstr "ממתין לרשומות מספיקות לתצוגה"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "פקודת Windows" msgstr "פקודת Windows"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "כתיבה" msgstr "כתיבה"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n" "Language: hr\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} dostupno"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "1 sat" msgstr "1 sat"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 minut" msgstr "1 minut"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 sati" msgstr "12 sati"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 minuta" msgstr "15 minuta"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 dana" msgstr "30 dana"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 minuta" msgstr "5 minuta"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Aktivno stanje" msgstr "Aktivno stanje"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Dodaj {foo}" msgstr "Dodaj {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Dodaj <0>Sustav</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Dodaj sustav"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "Dodaj URL" msgstr "Dodaj URL"
@@ -134,8 +135,9 @@ msgstr "Prilagodite širinu glavnog rasporeda"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr ""
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "After" msgid "After"
@@ -147,7 +149,7 @@ msgstr "Nakon postavljanja varijabli okruženja, ponovno pokrenite svoj Beszel h
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Agent" msgid "Agent"
msgstr "Agent" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -163,6 +165,7 @@ msgstr "Upozorenja"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Svi spremnici" msgstr "Svi spremnici"
@@ -188,11 +191,11 @@ msgstr "Jeste li sigurni?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Automatsko kopiranje zahtijeva siguran kontekst." msgstr "Automatsko kopiranje zahtijeva siguran kontekst."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Prosjek" msgstr "Prosjek"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Prosječna iskorištenost procesora u spremnicima" msgstr "Prosječna iskorištenost procesora u spremnicima"
@@ -206,20 +209,29 @@ msgstr "Prosjek pada ispod <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Prosjek premašuje <0>{value}{0}</0>" msgstr "Prosjek premašuje <0>{value}{0}</0>"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Prosječan broj I/O operacija koje čekaju na obradu"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "Prosječna potrošnja energije grafičkog procesora" msgstr "Prosječna potrošnja energije grafičkog procesora"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Prosječno vrijeme od čekanja do završetka po operaciji"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Prosječna iskorištenost procesora u cijelom sustavu" msgstr "Prosječna iskorištenost procesora u cijelom sustavu"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Prosječna iskorištenost {0}" msgstr "Prosječna iskorištenost {0}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "Prosječna iskorištenost grafičkih procesora" msgstr "Prosječna iskorištenost grafičkih procesora"
@@ -228,7 +240,7 @@ msgstr "Prosječna iskorištenost grafičkih procesora"
msgid "Backups" msgid "Backups"
msgstr "Sigurnosne kopije" msgstr "Sigurnosne kopije"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Mrežna Propusnost" msgstr "Mrežna Propusnost"
@@ -236,9 +248,9 @@ msgstr "Mrežna Propusnost"
#. Battery label in systems table header #. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Bat" msgid "Bat"
msgstr "Bat" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Baterija" msgstr "Baterija"
@@ -288,7 +300,7 @@ msgstr "Stanje pokretanja"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bajtovi (KB/s, MB/s, GB/s)" msgstr "Bajtovi (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Predmemorija / Međuspremnici" msgstr "Predmemorija / Međuspremnici"
@@ -324,7 +336,7 @@ msgstr "Oprez - mogući gubitak podataka"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -334,7 +346,7 @@ msgstr "Promijenite mjerene jedinice korištene za prikazivanje podataka."
msgid "Change general application options." msgid "Change general application options."
msgstr "Promijenite opće opcije aplikacije." msgstr "Promijenite opće opcije aplikacije."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Punjenje" msgstr "Punjenje"
@@ -347,6 +359,10 @@ msgstr "Puni se"
msgid "Chart options" msgid "Chart options"
msgstr "Postavke grafikona" msgstr "Postavke grafikona"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Širina grafikona"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Provjerite {email} za pristup poveznici za resetiranje." msgstr "Provjerite {email} za pristup poveznici za resetiranje."
@@ -407,6 +423,10 @@ msgstr "Sukobi"
msgid "Connection is down" msgid "Connection is down"
msgstr "Veza je pala" msgstr "Veza je pala"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Kontejneri"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Kopiraj env" msgstr "Kopiraj env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopiraj iz"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Kopiraj hosta" msgstr "Kopiraj hosta"
@@ -462,6 +487,11 @@ msgstr "Kopirajte sadržaj <0>docker-compose.yml</0> datoteke za opisanog agenta
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Kopiraj YAML" msgstr "Kopiraj YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Jezgra"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -484,8 +514,8 @@ msgstr "CPU vrijeme"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "Raspodjela CPU vremena" msgstr "Raspodjela CPU vremena"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Kumulativno otpremanje" msgstr "Kumulativno otpremanje"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Trenutno stanje" msgstr "Trenutno stanje"
@@ -531,6 +561,11 @@ msgstr "Ciklusi"
msgid "Daily" msgid "Daily"
msgstr "Dnevno" msgstr "Dnevno"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Zadano"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Zadano vremensko razdoblje" msgstr "Zadano vremensko razdoblje"
@@ -563,37 +598,43 @@ msgstr "Uređaj"
msgid "Discharging" msgid "Discharging"
msgstr "Prazni se" msgstr "Prazni se"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disk" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Disk I/O" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Disk unit" msgid "Disk unit"
msgstr "Mjerna jedinica za disk" msgstr "Mjerna jedinica za disk"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Iskorištenost Diska" msgstr "Iskorištenost Diska"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Iskorištenost diska od {extraFsName}" msgstr "Iskorištenost diska od {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Prikaz"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Iskorištenost Docker procesora" msgstr "Iskorištenost Docker procesora"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Iskorištenost Docker memorije" msgstr "Iskorištenost Docker memorije"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Docker mrežni I/O" msgstr "Docker mrežni I/O"
@@ -636,7 +677,7 @@ msgstr "Uredi {foo}"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx #: src/components/login/otp-forms.tsx
msgid "Email" msgid "Email"
msgstr "Email" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Email notifications" msgid "Email notifications"
@@ -770,7 +811,7 @@ msgstr "Neuspjelo: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD naredba" msgstr "FreeBSD naredba"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Puno" msgstr "Puno"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "Globalno" msgstr "Globalno"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "Grafički procesori" msgstr "Grafički procesori"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "Energetska potrošnja grafičkog procesora" msgstr "Energetska potrošnja grafičkog procesora"
@@ -826,6 +872,7 @@ msgstr "Energetska potrošnja grafičkog procesora"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "Iskorištenost GPU-a" msgstr "Iskorištenost GPU-a"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Rešetka" msgstr "Rešetka"
@@ -836,7 +883,7 @@ msgstr "Zdravlje"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -854,7 +901,7 @@ msgstr "Homebrew naredba"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Host / IP" msgid "Host / IP"
msgstr "Host / IP" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method" msgid "HTTP Method"
@@ -864,6 +911,21 @@ msgstr "HTTP metoda"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metoda: POST, GET ili HEAD (zadano: POST)" msgstr "HTTP metoda: POST, GET ili HEAD (zadano: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O čekanje"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O vrijeme"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O iskorištenost"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -884,7 +946,7 @@ msgstr "Neaktivno"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Interval" msgid "Interval"
msgstr "Interval" msgstr ""
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Invalid email address." msgid "Invalid email address."
@@ -912,7 +974,7 @@ msgstr "Životni ciklus"
msgid "limit" msgid "limit"
msgstr "ograničenje" msgstr "ograničenje"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Prosječno Opterećenje" msgstr "Prosječno Opterećenje"
@@ -941,6 +1003,7 @@ msgstr "Stanje učitavanja"
msgid "Loading..." msgid "Loading..."
msgstr "Učitavanje..." msgstr "Učitavanje..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Odjava" msgstr "Odjava"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Upute za ručno postavljanje" msgstr "Upute za ručno postavljanje"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Maksimalno 1 minuta" msgstr "Maksimalno 1 minuta"
@@ -999,18 +1062,19 @@ msgstr "Ograničenje memorije"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Vrhunac memorije" msgstr "Vrhunac memorije"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Iskorištenost memorije" msgstr "Iskorištenost memorije"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Iskorištenost memorije Docker spremnika" msgstr "Iskorištenost memorije Docker spremnika"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Model" msgstr ""
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
@@ -1025,11 +1089,11 @@ msgstr "Ime"
msgid "Net" msgid "Net"
msgstr "Mreža" msgstr "Mreža"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Mrežni promet Docker spremnika" msgstr "Mrežni promet Docker spremnika"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "Format korisnog tereta (Payload)"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Prosječna iskorištenost po jezgri" msgstr "Prosječna iskorištenost po jezgri"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Postotak vremena u kojem je disk zauzet I/O operacijama"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Postotak vremena provedenog u svakom stanju" msgstr "Postotak vremena provedenog u svakom stanju"
@@ -1218,17 +1286,22 @@ msgstr "Molimo prijavite se u svoj račun"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
msgstr "Port" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Portovi"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Uključivanje" msgstr "Uključivanje"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Precizno iskorištenje u zabilježenom vremenu" msgstr "Precizno iskorištenje u zabilježenom trenutku"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Preferred Language" msgid "Preferred Language"
@@ -1243,17 +1316,24 @@ msgstr "Proces pokrenut"
msgid "Public Key" msgid "Public Key"
msgstr "Javni Ključ" msgstr "Javni Ključ"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Dubina reda"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Tihi sati" msgstr "Tihi sati"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Pročitaj" msgstr "Pročitaj"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Primljeno" msgstr "Primljeno"
@@ -1326,19 +1406,19 @@ msgstr "S.M.A.R.T. Detalji"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T. Samotestiranje" msgstr "S.M.A.R.T. Samotestiranje"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "Spremi {foo}"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Spremite adresu pomoću tipke enter ili zareza. Ostavite prazno kako biste onemogućili obavijesti e-poštom." msgstr "Spremite adresu pomoću tipke enter ili zarez. Ostavite prazno kako biste onemogućili obavijesti e-poštom."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save Settings" msgid "Save Settings"
msgstr "Spremi Postavke" msgstr "Spremi Postavke"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Spremi sustav"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Spremljeno u bazi podataka i ne istječe dok ga ne onemogućite." msgstr "Spremljeno u bazi podataka i ne istječe dok ga ne onemogućite."
@@ -1361,7 +1441,7 @@ msgstr "Pretraži"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "Search for systems or settings..." msgid "Search for systems or settings..."
msgstr "Pretraži za sisteme ili postavke..." msgstr "Pretraži sustave ili postavke..."
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)" msgid "Seconds between pings (default: 60)"
@@ -1369,7 +1449,7 @@ msgstr "Sekunde između pingova (zadano: 60)"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način primanja upozorenja." msgstr "Pogledajte <0>postavke obavijesti</0> kako biste konfigurirali način primanja upozorenja."
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}" msgid "Select {foo}"
@@ -1387,7 +1467,7 @@ msgstr "Šaljite povremene odlazne pingove vanjskoj usluzi nadzora kako biste mo
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Pošalji testni heartbeat" msgstr "Pošalji testni heartbeat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Poslano" msgstr "Poslano"
@@ -1399,6 +1479,7 @@ msgstr "Serijski broj"
msgid "Service Details" msgid "Service Details"
msgstr "Detalji usluge" msgstr "Detalji usluge"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Usluge" msgstr "Usluge"
@@ -1414,8 +1495,10 @@ msgstr "Postavite sljedeće varijable okruženja na svom Beszel hubu kako biste
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Postavke" msgstr "Postavke"
@@ -1453,23 +1536,24 @@ msgstr "Stanje"
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr ""
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
msgid "Sub State" msgid "Sub State"
msgstr "Podstanje" msgstr "Podstanje"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Swap prostor uzet od strane sistema" msgstr "Swap prostor kojeg je zauzeo sustav"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap Iskorištenost" msgstr "Swap Iskorištenost"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1479,9 +1563,9 @@ msgstr "Swap Iskorištenost"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "System" msgid "System"
msgstr "Sistem" msgstr "Sustav"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Prosječno opterećenje sustava kroz vrijeme" msgstr "Prosječno opterećenje sustava kroz vrijeme"
@@ -1495,12 +1579,17 @@ msgstr "Sustavi"
#: src/components/routes/settings/config-yaml.tsx #: src/components/routes/settings/config-yaml.tsx
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory." msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
msgstr "Sistemima se može upravljati u <0>config.yml</0> datoteci unutar data direktorija." msgstr "Sustavima se može upravljati pomoću <0>config.yml</0> datoteke unutar data mape."
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Table" msgid "Table"
msgstr "Tablica" msgstr "Tablica"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Kartice"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Zadaci" msgstr "Zadaci"
@@ -1509,9 +1598,9 @@ msgstr "Zadaci"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Temp" msgid "Temp"
msgstr "Temp" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperatura" msgstr "Temperatura"
@@ -1520,13 +1609,13 @@ msgstr "Temperatura"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Mjerna jedinica za temperaturu" msgstr "Mjerna jedinica za temperaturu"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "Temperature sistemskih senzora" msgstr "Temperature sustavnih mjerila"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>" msgid "Test <0>URL</0>"
msgstr "Testni <0>URL</0>" msgstr "Probni <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat" msgid "Test heartbeat"
@@ -1534,7 +1623,7 @@ msgstr "Testiraj heartbeat"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Test notification sent" msgid "Test notification sent"
msgstr "Testna obavijest poslana" msgstr "Probna obavijest poslana"
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down." msgid "The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down."
@@ -1546,17 +1635,17 @@ msgstr "Zatim se prijavite u backend i resetirajte lozinku korisničkog računa
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database." msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
msgstr "Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve trenutne zapise za {name} iz baze podataka." msgstr "Ova radnja ne može se poništiti. Svi trenutni zapisi za {name} bit će trajno izbrisani iz baze podataka."
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Ovom radnjom će se trajno izbrisati svi odabrani zapisi iz baze podataka." msgstr "Ovom radnjom će se trajno izbrisati svi odabrani zapisi iz baze podataka."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "Protok {extraFsName}" msgstr "Protok {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "Protok root datotečnog sustava" msgstr "Protok root datotečnog sustava"
@@ -1566,12 +1655,7 @@ msgstr "Format vremena"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "To email(s)" msgid "To email(s)"
msgstr "Primaoci e-pošte" msgstr "Primaoci emaila"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Uključi/isključi rešetku"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
@@ -1581,7 +1665,7 @@ msgstr "Uključi/isključi temu"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1610,6 +1694,11 @@ msgstr "Ukupni podaci primljeni za svako sučelje"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Ukupni podaci poslani za svako sučelje" msgstr "Ukupni podaci poslani za svako sučelje"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1645,7 +1734,7 @@ msgstr "Pokreće se kada razina baterije padne ispod praga"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when combined up/down exceeds a threshold" msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Pokreće se kada kombinacija gore/dolje premaši prag" msgstr "Pokreće se kada kumulativna propusnost gore/dolje premaši prag"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when CPU usage exceeds a threshold" msgid "Triggers when CPU usage exceeds a threshold"
@@ -1661,7 +1750,7 @@ msgstr "Pokreće se kada iskorištenost memorije premaši prag"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when status switches between up and down" msgid "Triggers when status switches between up and down"
msgstr "Pokreće se kada se status sistema promijeni" msgstr "Pokreće se kada se status sustava promijeni"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when usage of any disk exceeds a threshold" msgid "Triggers when usage of any disk exceeds a threshold"
@@ -1690,7 +1779,7 @@ msgstr "Sveopći token"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Unknown" msgid "Unknown"
msgstr "Nepoznata" msgstr "Nepoznato"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
@@ -1728,22 +1817,22 @@ msgstr "Otpremi"
#: src/components/routes/system/info-bar.tsx #: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime" msgid "Uptime"
msgstr "Uptime" msgstr "Vrijeme rada"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Iskorištenost" msgstr "Iskorištenost"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Iskorištenost root datotečnog sustava" msgstr "Iskorištenost root datotečnog sustava"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Iskorišteno" msgstr "Iskorišteno"
@@ -1752,6 +1841,11 @@ msgstr "Iskorišteno"
msgid "Users" msgid "Users"
msgstr "Korisnici" msgstr "Korisnici"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Iskorištenost"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Vrijednost" msgstr "Vrijednost"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Prikaz" msgstr "Prikaz"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Prikaži više" msgstr "Prikaži više"
@@ -1773,13 +1868,13 @@ msgstr "Pogledajte posljednjih 200 upozorenja."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Vidljiva polja" msgstr "Vidljiva polja"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Čeka se na više podataka prije prikaza" msgstr "Potrebno je više podataka za prikaz"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details." msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "Želite li nam pomoći da naše prijevode učinimo još boljim? Posjetite <0>Crowdin</0> za više detalja." msgstr "Želite li nam pomoći poboljšati prijevode? Posjetite <0>Crowdin</0> za više detalja."
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Wants" msgid "Wants"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Windows naredba" msgstr "Windows naredba"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Piši" msgstr "Piši"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n" "Language: hu\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n" "PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -18,6 +18,12 @@ msgstr ""
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n" "X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n" "X-Crowdin-File-ID: 32\n"
#. placeholder {0}: newVersion.v
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} elérhető"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
@@ -49,7 +55,7 @@ msgid "1 hour"
msgstr "1 óra" msgstr "1 óra"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 perc" msgstr "1 perc"
@@ -66,7 +72,7 @@ msgid "12 hours"
msgstr "12 óra" msgstr "12 óra"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 perc" msgstr "15 perc"
@@ -79,7 +85,7 @@ msgid "30 days"
msgstr "30 nap" msgstr "30 nap"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 perc" msgstr "5 perc"
@@ -107,19 +113,14 @@ msgid "Active state"
msgstr "Aktív állapot" msgstr "Aktív állapot"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/add-system.tsx
#: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}" msgid "Add {foo}"
msgstr "Hozzáadás {foo}" msgstr "Hozzáadás {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "<0>Rendszer</0> Hozzáadása"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Rendszer hozzáadása"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Add URL" msgid "Add URL"
msgstr "URL hozzáadása" msgstr "URL hozzáadása"
@@ -134,6 +135,7 @@ msgstr "A fő elrendezés szélességének beállítása"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin" msgid "Admin"
msgstr "Adminisztráció" msgstr "Adminisztráció"
@@ -163,6 +165,7 @@ msgstr "Riasztások"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Minden konténer" msgstr "Minden konténer"
@@ -188,11 +191,11 @@ msgstr "Biztos vagy benne?"
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
msgstr "Az automatikus másolás biztonságos környezetet igényel." msgstr "Az automatikus másolás biztonságos környezetet igényel."
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Average" msgid "Average"
msgstr "Átlag" msgstr "Átlag"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Konténerek átlagos CPU kihasználtsága" msgstr "Konténerek átlagos CPU kihasználtsága"
@@ -206,20 +209,29 @@ msgstr "Az átlag esik <0>{value}{0}</0> alá"
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Az átlag meghaladja a <0>{value}{0}</0> értéket" msgstr "Az átlag meghaladja a <0>{value}{0}</0> értéket"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Kiszolgálásra váró I/O műveletek átlagos száma"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs" msgid "Average power consumption of GPUs"
msgstr "GPU-k átlagos energiafogyasztása" msgstr "GPU-k átlagos energiafogyasztása"
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "Average queue to completion time per operation"
msgstr "Műveletenkénti átlagos várakozási és befejezési idő"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization" msgid "Average system-wide CPU utilization"
msgstr "Rendszerszintű CPU átlagos kihasználtság" msgstr "Rendszerszintű CPU átlagos kihasználtság"
#. placeholder {0}: gpu.n #. placeholder {0}: gpu.n
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "{0} átlagos kihasználtsága" msgstr "{0} átlagos kihasználtsága"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average utilization of GPU engines" msgid "Average utilization of GPU engines"
msgstr "GPU-k átlagos kihasználtsága" msgstr "GPU-k átlagos kihasználtsága"
@@ -228,7 +240,7 @@ msgstr "GPU-k átlagos kihasználtsága"
msgid "Backups" msgid "Backups"
msgstr "Biztonsági mentések" msgstr "Biztonsági mentések"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Sávszélesség" msgstr "Sávszélesség"
@@ -238,7 +250,7 @@ msgstr "Sávszélesség"
msgid "Bat" msgid "Bat"
msgstr "Akku" msgstr "Akku"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Battery" msgid "Battery"
msgstr "Akkumulátor" msgstr "Akkumulátor"
@@ -288,7 +300,7 @@ msgstr "Indítási állapot"
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Byte-ok (KB/s, MB/s, GB/s)" msgstr "Byte-ok (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Gyorsítótár / Pufferelések" msgstr "Gyorsítótár / Pufferelések"
@@ -324,7 +336,7 @@ msgstr "Figyelem - potenciális adatvesztés"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Celsius (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -334,7 +346,7 @@ msgstr "A mértékegységek megjelenítésének megváltoztatása."
msgid "Change general application options." msgid "Change general application options."
msgstr "Általános alkalmazásbeállítások módosítása." msgstr "Általános alkalmazásbeállítások módosítása."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Charge" msgid "Charge"
msgstr "Töltés" msgstr "Töltés"
@@ -347,6 +359,10 @@ msgstr "Töltődik"
msgid "Chart options" msgid "Chart options"
msgstr "Diagram beállítások" msgstr "Diagram beállítások"
#: src/components/routes/system/info-bar.tsx
msgid "Chart width"
msgstr "Grafikon szélessége"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link." msgid "Check {email} for a reset link."
msgstr "Ellenőrizd a {email} címet a visszaállító linkért." msgstr "Ellenőrizd a {email} címet a visszaállító linkért."
@@ -407,6 +423,10 @@ msgstr "Konfliktusok"
msgid "Connection is down" msgid "Connection is down"
msgstr "Kapcsolat megszakadt" msgstr "Kapcsolat megszakadt"
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Konténerek"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Continue" msgid "Continue"
@@ -433,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Környezet másolása" msgstr "Környezet másolása"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Másolás innen:"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
msgstr "Hoszt másolása" msgstr "Hoszt másolása"
@@ -462,11 +487,16 @@ msgstr "Másold az alábbi ügynök <0>docker-compose.yml</0> tartalmát, vagy r
msgid "Copy YAML" msgid "Copy YAML"
msgstr "YAML másolása" msgstr "YAML másolása"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Mag"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx #: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr ""
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
@@ -484,8 +514,8 @@ msgstr "CPU idő"
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "CPU idő felbontása" msgstr "CPU idő felbontása"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/cpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
@@ -517,7 +547,7 @@ msgid "Cumulative Upload"
msgstr "Kumulatív feltöltés" msgstr "Kumulatív feltöltés"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Current state" msgid "Current state"
msgstr "Jelenlegi állapot" msgstr "Jelenlegi állapot"
@@ -531,6 +561,11 @@ msgstr "Ciklusok"
msgid "Daily" msgid "Daily"
msgstr "Napi" msgstr "Napi"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Alapértelmezett"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Default time period" msgid "Default time period"
msgstr "Alapértelmezett időszak" msgstr "Alapértelmezett időszak"
@@ -563,11 +598,12 @@ msgstr "Eszköz"
msgid "Discharging" msgid "Discharging"
msgstr "Kisül" msgstr "Kisül"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Lemez" msgstr "Lemez"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O" msgid "Disk I/O"
msgstr "Lemez I/O" msgstr "Lemez I/O"
@@ -575,25 +611,30 @@ msgstr "Lemez I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "Lemez mértékegysége" msgstr "Lemez mértékegysége"
#: src/components/charts/disk-chart.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Lemezhasználat" msgstr "Lemezhasználat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}" msgid "Disk usage of {extraFsName}"
msgstr "Lemezhasználat a {extraFsName}" msgstr "Lemezhasználat a {extraFsName}"
#: src/components/routes/system.tsx #: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Megjelenítés"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage" msgid "Docker CPU Usage"
msgstr "Docker CPU használat" msgstr "Docker CPU használat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Docker Memory Usage" msgid "Docker Memory Usage"
msgstr "Docker memória használat" msgstr "Docker memória használat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Docker Network I/O" msgid "Docker Network I/O"
msgstr "Docker hálózat I/O" msgstr "Docker hálózat I/O"
@@ -636,7 +677,7 @@ msgstr "Szerkesztés {foo}"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx #: src/components/login/otp-forms.tsx
msgid "Email" msgid "Email"
msgstr "Email" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Email notifications" msgid "Email notifications"
@@ -731,7 +772,7 @@ msgstr "Exportálja a jelenlegi rendszerkonfigurációt."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Failed" msgid "Failed"
@@ -770,7 +811,7 @@ msgstr "Sikertelen: {0}"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -783,7 +824,7 @@ msgstr "Ujjlenyomat"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Firmware" msgid "Firmware"
msgstr "Firmware" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -800,6 +841,7 @@ msgid "FreeBSD command"
msgstr "FreeBSD parancs" msgstr "FreeBSD parancs"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system/info-bar.tsx
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "Tele" msgstr "Tele"
@@ -815,10 +857,14 @@ msgid "Global"
msgstr "Globális" msgstr "Globális"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines" msgid "GPU Engines"
msgstr "GPU-k" msgstr "GPU-k"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Power Draw" msgid "GPU Power Draw"
msgstr "GPU áramfelvétele" msgstr "GPU áramfelvétele"
@@ -826,6 +872,7 @@ msgstr "GPU áramfelvétele"
msgid "GPU Usage" msgid "GPU Usage"
msgstr "GPU használat" msgstr "GPU használat"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Grid" msgid "Grid"
msgstr "Rács" msgstr "Rács"
@@ -836,7 +883,7 @@ msgstr "Egészség"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Heartbeat" msgid "Heartbeat"
msgstr "Heartbeat" msgstr ""
#: src/components/routes/settings/heartbeat.tsx #: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring" msgid "Heartbeat Monitoring"
@@ -864,6 +911,21 @@ msgstr "HTTP metódus"
msgid "HTTP method: POST, GET, or HEAD (default: POST)" msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metódus: POST, GET vagy HEAD (alapértelmezett: POST)" msgstr "HTTP metódus: POST, GET vagy HEAD (alapértelmezett: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "I/O várakozás"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "I/O idő"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "I/O kihasználtság"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
@@ -912,7 +974,7 @@ msgstr "Életciklus"
msgid "limit" msgid "limit"
msgstr "korlát" msgstr "korlát"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Terhelési átlag" msgstr "Terhelési átlag"
@@ -941,6 +1003,7 @@ msgstr "Betöltési állapot"
msgid "Loading..." msgid "Loading..."
msgstr "Betöltés..." msgstr "Betöltés..."
#: src/components/navbar.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
msgstr "Kijelentkezés" msgstr "Kijelentkezés"
@@ -978,7 +1041,7 @@ msgid "Manual setup instructions"
msgstr "Manuális beállítási lépések" msgstr "Manuális beállítási lépések"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Maximum 1 perc" msgstr "Maximum 1 perc"
@@ -999,15 +1062,16 @@ msgstr "Memória korlát"
msgid "Memory Peak" msgid "Memory Peak"
msgstr "Memória csúcs" msgstr "Memória csúcs"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Memóriahasználat" msgstr "Memóriahasználat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Docker konténerek memória használata" msgstr "Docker konténerek memória használata"
#. Device model
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "Modell" msgstr "Modell"
@@ -1025,11 +1089,11 @@ msgstr "Név"
msgid "Net" msgid "Net"
msgstr "Hálózat" msgstr "Hálózat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Docker konténerek hálózati forgalma" msgstr "Docker konténerek hálózati forgalma"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -1171,6 +1235,10 @@ msgstr "Payload formátum"
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Átlagos kihasználtság magonként" msgstr "Átlagos kihasználtság magonként"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Az az időtartam százalékban, amíg a lemez I/O műveletekkel van elfoglalva"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
msgstr "Az idő százalékos aránya minden állapotban" msgstr "Az idő százalékos aránya minden állapotban"
@@ -1218,15 +1286,20 @@ msgstr "Kérjük, jelentkezzen be a fiókjába"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
msgstr "Port" msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Portok"
#. Power On Time #. Power On Time
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Power On" msgid "Power On"
msgstr "Bekapcsolás" msgstr "Bekapcsolás"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Precise utilization at the recorded time" msgid "Precise utilization at the recorded time"
msgstr "Pontos kihasználás a rögzített időpontban" msgstr "Pontos kihasználás a rögzített időpontban"
@@ -1243,17 +1316,24 @@ msgstr "Folyamat elindítva"
msgid "Public Key" msgid "Public Key"
msgstr "Nyilvános kulcs" msgstr "Nyilvános kulcs"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Várakozási sor mélysége"
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours" msgid "Quiet Hours"
msgstr "Csendes órák" msgstr "Csendes órák"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Read" msgid "Read"
msgstr "Olvasás" msgstr "Olvasás"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Received" msgid "Received"
msgstr "Fogadott" msgstr "Fogadott"
@@ -1326,6 +1406,10 @@ msgstr "S.M.A.R.T. Részletek"
msgid "S.M.A.R.T. Self-Test" msgid "S.M.A.R.T. Self-Test"
msgstr "S.M.A.R.T. Önteszt" msgstr "S.M.A.R.T. Önteszt"
#: src/components/add-system.tsx
msgid "Save {foo}"
msgstr "{foo} mentése"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Mentse el a címet az Enter billentyű vagy a vessző használatával. Hagyja üresen az e-mail értesítések letiltásához." msgstr "Mentse el a címet az Enter billentyű vagy a vessző használatával. Hagyja üresen az e-mail értesítések letiltásához."
@@ -1335,10 +1419,6 @@ msgstr "Mentse el a címet az Enter billentyű vagy a vessző használatával. H
msgid "Save Settings" msgid "Save Settings"
msgstr "Beállítások mentése" msgstr "Beállítások mentése"
#: src/components/add-system.tsx
msgid "Save system"
msgstr "Rendszer mentése"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it." msgid "Saved in the database and does not expire until you disable it."
msgstr "Elmentve az adatbázisban és nem jár le, amíg ki nem kapcsolod." msgstr "Elmentve az adatbázisban és nem jár le, amíg ki nem kapcsolod."
@@ -1387,7 +1467,7 @@ msgstr "Küldjön időszakos kimenő pingeket egy külső megfigyelő szolgálta
msgid "Send test heartbeat" msgid "Send test heartbeat"
msgstr "Teszt heartbeat küldése" msgstr "Teszt heartbeat küldése"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/network-charts.tsx
msgid "Sent" msgid "Sent"
msgstr "Elküldve" msgstr "Elküldve"
@@ -1399,6 +1479,7 @@ msgstr "Sorozatszám"
msgid "Service Details" msgid "Service Details"
msgstr "Szolgáltatás részletei" msgstr "Szolgáltatás részletei"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Services" msgid "Services"
msgstr "Szolgáltatások" msgstr "Szolgáltatások"
@@ -1414,8 +1495,10 @@ msgstr "Állítsa be a következő környezeti változókat a Beszel hubon a hea
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Settings" msgid "Settings"
msgstr "Beállítások" msgstr "Beállítások"
@@ -1459,17 +1542,18 @@ msgstr "Állapot"
msgid "Sub State" msgid "Sub State"
msgstr "Részállapot" msgstr "Részállapot"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap space used by the system" msgid "Swap space used by the system"
msgstr "Rendszer által használt swap terület" msgstr "Rendszer által használt swap terület"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap használat" msgstr "Swap használat"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx #: src/components/routes/settings/quiet-hours.tsx
@@ -1481,7 +1565,7 @@ msgstr "Swap használat"
msgid "System" msgid "System"
msgstr "Rendszer" msgstr "Rendszer"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Rendszer terhelési átlaga" msgstr "Rendszer terhelési átlaga"
@@ -1501,6 +1585,11 @@ msgstr "A rendszereket egy <0>config.yml</0> fájlban lehet kezelni az adatköny
msgid "Table" msgid "Table"
msgstr "Tábla" msgstr "Tábla"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Lapok"
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Tasks" msgid "Tasks"
msgstr "Feladatok" msgstr "Feladatok"
@@ -1511,7 +1600,7 @@ msgstr "Feladatok"
msgid "Temp" msgid "Temp"
msgstr "Hőmérséklet" msgstr "Hőmérséklet"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Hőmérséklet" msgstr "Hőmérséklet"
@@ -1520,7 +1609,7 @@ msgstr "Hőmérséklet"
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Hőmérséklet mértékegysége" msgstr "Hőmérséklet mértékegysége"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/sensor-charts.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
msgstr "A rendszer érzékelőinek hőmérséklete" msgstr "A rendszer érzékelőinek hőmérséklete"
@@ -1552,11 +1641,11 @@ msgstr "Ezt a műveletet nem lehet visszavonni! Véglegesen törli a {name} öss
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Ez véglegesen törli az összes kijelölt bejegyzést az adatbázisból." msgstr "Ez véglegesen törli az összes kijelölt bejegyzést az adatbázisból."
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
msgstr "A {extraFsName} átviteli teljesítménye" msgstr "A {extraFsName} átviteli teljesítménye"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of root filesystem" msgid "Throughput of root filesystem"
msgstr "A gyökér fájlrendszer átviteli teljesítménye" msgstr "A gyökér fájlrendszer átviteli teljesítménye"
@@ -1568,11 +1657,6 @@ msgstr "Időformátum"
msgid "To email(s)" msgid "To email(s)"
msgstr "E-mailben" msgstr "E-mailben"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Rács ki- és bekapcsolása"
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx #: src/components/mode-toggle.tsx
msgid "Toggle theme" msgid "Toggle theme"
@@ -1581,7 +1665,7 @@ msgstr "Téma váltása"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Token" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1610,6 +1694,11 @@ msgstr "Összes fogadott adat minden interfészenként"
msgid "Total data sent for each interface" msgid "Total data sent for each interface"
msgstr "Összes elküldött adat minden interfészenként" msgstr "Összes elküldött adat minden interfészenként"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O"
msgid "Total time spent on read/write (can exceed 100%)"
msgstr ""
#. placeholder {0}: data.length #. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx #: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}" msgid "Total: {0}"
@@ -1730,20 +1819,20 @@ msgstr "Feltöltés"
msgid "Uptime" msgid "Uptime"
msgstr "Üzemidő" msgstr "Üzemidő"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Usage" msgid "Usage"
msgstr "Használat" msgstr "Használat"
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Root partíció kihasználtsága" msgstr "Root partíció kihasználtsága"
#: src/components/charts/mem-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
#: src/components/charts/swap-chart.tsx #: src/components/routes/system/charts/memory-charts.tsx
msgid "Used" msgid "Used"
msgstr "Felhasznált" msgstr "Felhasznált"
@@ -1752,6 +1841,11 @@ msgstr "Felhasznált"
msgid "Users" msgid "Users"
msgstr "Felhasználók" msgstr "Felhasználók"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Kihasználtság"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Érték" msgstr "Érték"
@@ -1761,6 +1855,7 @@ msgid "View"
msgstr "Nézet" msgstr "Nézet"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "View more" msgid "View more"
msgstr "Továbbiak megjelenítése" msgstr "Továbbiak megjelenítése"
@@ -1773,7 +1868,7 @@ msgstr "Legfrissebb 200 riasztásod áttekintése."
msgid "Visible Fields" msgid "Visible Fields"
msgstr "Látható mezők" msgstr "Látható mezők"
#: src/components/routes/system.tsx #: src/components/routes/system/chart-card.tsx
msgid "Waiting for enough records to display" msgid "Waiting for enough records to display"
msgstr "Elegendő rekordra várva a megjelenítéshez" msgstr "Elegendő rekordra várva a megjelenítéshez"
@@ -1812,8 +1907,10 @@ msgid "Windows command"
msgstr "Windows parancs" msgstr "Windows parancs"
#. Disk write #. Disk write
#: src/components/routes/system.tsx #: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system.tsx #: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Write" msgid "Write"
msgstr "Írás" msgstr "Írás"

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