Compare commits

..

35 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
95 changed files with 9064 additions and 1305 deletions

View File

@@ -19,6 +19,8 @@ import (
gossh "golang.org/x/crypto/ssh"
)
const defaultDataCacheTimeMs uint16 = 60_000
type Agent struct {
sync.Mutex // Used to lock agent while collecting data
debug bool // true if LOG_LEVEL is set to debug
@@ -36,6 +38,7 @@ type Agent struct {
sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info (dynamic)
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
cache *systemDataCache // Cache for system stats based on cache time
connectionManager *ConnectionManager // Channel to signal connection events
@@ -97,7 +100,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
slog.Debug(beszel.Version)
// initialize docker manager
agent.dockerManager = newDockerManager()
agent.dockerManager = newDockerManager(agent)
// initialize system info
agent.refreshSystemDetails()
@@ -142,7 +145,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// if debugging, print stats
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
@@ -164,11 +167,6 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD
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)
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
if a.systemdManager != nil && cacheTimeMs == 60_000 {
if a.systemdManager != nil && cacheTimeMs == defaultDataCacheTimeMs {
totalCount := uint16(a.systemdManager.getServiceStatsCount())
if totalCount > 0 {
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)
a.cache.Set(data, cacheTimeMs)
return data
return a.attachSystemDetails(data, cacheTimeMs, options.IncludeDetails)
}
// 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 to get the battery stats.
// Package battery provides functions to check if the system has a battery and return the charge state and percentage.
package battery
import (
"errors"
"log/slog"
"math"
"github.com/distatus/battery"
const (
stateUnknown uint8 = iota
stateEmpty
stateFull
stateCharging
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

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
import (
"context"
"log/slog"
"os"
"path/filepath"
@@ -33,6 +34,34 @@ type diskDiscovery struct {
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"
// Returns the device/filesystem part and the custom name part
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
// 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) {
if !strings.HasPrefix(p.Mountpoint, d.ctx.efPath) {
if filepath.Dir(p.Mountpoint) != d.ctx.efPath {
return
}
device, customName := extraFilesystemPartitionInfo(p)
@@ -273,7 +304,7 @@ func (a *Agent) initializeDiskInfo() {
hasRoot := false
isWindows := runtime.GOOS == "windows"
partitions, err := disk.Partitions(false)
partitions, err := disk.PartitionsWithContext(context.Background(), true)
if err != nil {
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]
if !hasPrev {
// 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() {
prev = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
prev = prevDiskFromCounter(d, now)
}
}
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 {
// 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
}
@@ -599,15 +643,38 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
// validate values
if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {
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
a.initializeDiskIoStats(ioCounters)
continue
}
// Update per-interval snapshot
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
// These properties are calculated differently on different platforms,
// 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
stats.Time = now
@@ -617,20 +684,40 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
stats.DiskWritePs = writeMbPerSecond
stats.DiskReadBytes = diskIORead
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 {
systemStats.DiskReadPs = stats.DiskReadPs
systemStats.DiskWritePs = stats.DiskWritePs
systemStats.DiskIO[0] = diskIORead
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 /
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
if osReleaseContent, err := os.ReadFile("/etc/os-release"); err == nil {
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) {
t.Run("matches by device name", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{

View File

@@ -25,6 +25,7 @@ import (
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/blang/semver"
)
@@ -52,20 +53,22 @@ const (
)
type dockerManager struct {
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList []*container.ApiInfo // List of containers from Docker API
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
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
agent *Agent // Used to propagate system detail changes back to the agent
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList []*container.ApiInfo // List of containers from Docker API
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
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
buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
// Maps cache time intervals to container-specific CPU usage tracking
@@ -78,7 +81,6 @@ type dockerManager struct {
networkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
networkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
lastNetworkReadTime map[uint16]map[string]time.Time // cacheTimeMs -> containerId -> last network read time
retrySleep func(time.Duration)
}
// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests
@@ -87,6 +89,14 @@ type userAgentRoundTripper struct {
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
func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", u.userAgent)
@@ -134,7 +144,14 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats,
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)
@@ -588,7 +605,7 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
}
// Creates a new http client for Docker or Podman API
func newDockerManager() *dockerManager {
func newDockerManager(agent *Agent) *dockerManager {
dockerHost, exists := utils.GetEnv("DOCKER_HOST")
if exists {
// return nil if set to empty string
@@ -654,6 +671,7 @@ func newDockerManager() *dockerManager {
}
manager := &dockerManager{
agent: agent,
client: &http.Client{
Timeout: timeout,
Transport: userAgentTransport,
@@ -671,51 +689,54 @@ func newDockerManager() *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),
retrySleep: time.Sleep,
}
// If using podman, return client
if strings.Contains(dockerHost, "podman") {
manager.usingPodman = true
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)
// Best-effort startup probe. If the engine is not ready yet, getDockerStats will
// retry after the first successful /containers/json request.
_, _ = manager.checkDockerVersion()
return manager
}
// 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.
func (dm *dockerManager) checkDockerVersion() {
var err error
var resp *http.Response
var versionInfo struct {
Version string `json:"Version"`
func (dm *dockerManager) checkDockerVersion() (bool, error) {
resp, err := dm.client.Get("http://localhost/version")
if err != nil {
return false, err
}
const versionMaxTries = 2
for i := 1; i <= versionMaxTries; i++ {
resp, err = dm.client.Get("http://localhost/version")
if err == nil && resp.StatusCode == http.StatusOK {
break
}
if resp != nil {
resp.Body.Close()
}
if i < versionMaxTries {
slog.Debug("Failed to get Docker version; retrying", "attempt", i, "err", err, "response", resp)
dm.retrySleep(5 * time.Second)
}
if resp.StatusCode != http.StatusOK {
status := resp.Status
resp.Body.Close()
return false, fmt.Errorf("docker version request failed: %s", status)
}
if err != nil || resp.StatusCode != http.StatusOK {
var versionInfo dockerVersionResponse
serverHeader := resp.Header.Get("Server")
if err := dm.decode(resp, &versionInfo); err != nil {
return false, err
}
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
}
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
}
// if version > 24, one-shot works correctly and we can limit concurrent operations
@@ -941,3 +962,46 @@ func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {
func (dm *dockerManager) IsPodman() bool {
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

@@ -539,59 +539,53 @@ func TestDockerManagerCreation(t *testing.T) {
func TestCheckDockerVersion(t *testing.T) {
tests := []struct {
name string
responses []struct {
statusCode int
body string
}
expectedGood bool
expectedRequests int
name string
statusCode int
body string
server string
expectSuccess bool
expectedGood bool
expectedPodman bool
expectError bool
expectedRequest string
}{
{
name: "200 with good version on first try",
responses: []struct {
statusCode int
body string
}{
{http.StatusOK, `{"Version":"25.0.1"}`},
},
expectedGood: true,
expectedRequests: 1,
name: "good docker version",
statusCode: http.StatusOK,
body: `{"Version":"25.0.1"}`,
expectSuccess: true,
expectedGood: true,
expectedPodman: false,
expectedRequest: "/version",
},
{
name: "200 with old version on first try",
responses: []struct {
statusCode int
body string
}{
{http.StatusOK, `{"Version":"24.0.7"}`},
},
expectedGood: false,
expectedRequests: 1,
name: "old docker version",
statusCode: http.StatusOK,
body: `{"Version":"24.0.7"}`,
expectSuccess: true,
expectedGood: false,
expectedPodman: false,
expectedRequest: "/version",
},
{
name: "non-200 then 200 with good version",
responses: []struct {
statusCode int
body string
}{
{http.StatusServiceUnavailable, `"not ready"`},
{http.StatusOK, `{"Version":"25.1.0"}`},
},
expectedGood: true,
expectedRequests: 2,
name: "podman from server header",
statusCode: http.StatusOK,
body: `{"Version":"5.5.0"}`,
server: "Libpod/5.5.0",
expectSuccess: true,
expectedGood: true,
expectedPodman: true,
expectedRequest: "/version",
},
{
name: "non-200 on all retries",
responses: []struct {
statusCode int
body string
}{
{http.StatusInternalServerError, `"error"`},
{http.StatusUnauthorized, `"error"`},
},
expectedGood: false,
expectedRequests: 2,
name: "non-200 response",
statusCode: http.StatusServiceUnavailable,
body: `"not ready"`,
expectSuccess: false,
expectedGood: false,
expectedPodman: false,
expectError: true,
expectedRequest: "/version",
},
}
@@ -599,13 +593,13 @@ func TestCheckDockerVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
idx := requestCount
requestCount++
if idx >= len(tt.responses) {
idx = len(tt.responses) - 1
assert.Equal(t, tt.expectedRequest, r.URL.EscapedPath())
if tt.server != "" {
w.Header().Set("Server", tt.server)
}
w.WriteHeader(tt.responses[idx].statusCode)
fmt.Fprint(w, tt.responses[idx].body)
w.WriteHeader(tt.statusCode)
fmt.Fprint(w, tt.body)
}))
defer server.Close()
@@ -617,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.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
dm := &dockerManager{
client: &http.Client{
@@ -638,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.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) {
dm := &dockerManager{
lastCpuContainer: map[uint16]map[string]uint64{

View File

@@ -542,7 +542,7 @@ func (gm *GPUManager) collectorDefinitions(caps gpuCapabilities) map[collectorSo
return map[collectorSource]collectorDefinition{
collectorSourceNVML: {
group: collectorGroupNvidia,
available: caps.hasNvidiaSmi,
available: true,
start: func(_ func()) bool {
return gm.startNvmlCollector()
},
@@ -734,9 +734,6 @@ func NewGPUManager() (*GPUManager, error) {
}
var gm GPUManager
caps := gm.discoverGpuCapabilities()
if !hasAnyGpuCollector(caps) {
return nil, fmt.Errorf(noGPUFoundMsg)
}
gm.GpuDataMap = make(map[string]*system.GPUData)
// Jetson devices should always use tegrastats (ignore GPU_COLLECTOR).
@@ -745,7 +742,7 @@ func NewGPUManager() (*GPUManager, error) {
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) != "" {
priorities := parseCollectorPriority(collectorConfig)
if gm.startCollectorsByPriority(priorities, caps) == 0 {
@@ -754,6 +751,10 @@ func NewGPUManager() (*GPUManager, error) {
return &gm, nil
}
if !hasAnyGpuCollector(caps) {
return nil, fmt.Errorf(noGPUFoundMsg)
}
// auto-detect and start collectors when GPU_COLLECTOR is unset.
if gm.startCollectorsByPriority(gm.resolveLegacyCollectorPriority(caps), caps) == 0 {
return nil, fmt.Errorf(noGPUFoundMsg)

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) {
dir := t.TempDir()
t.Setenv("PATH", dir)

View File

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

View File

@@ -19,13 +19,20 @@ import (
"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 {
context context.Context
sensors map[string]struct{}
primarySensor string
timeout time.Duration
isBlacklist bool
hasWildcards bool
skipCollection bool
firstRun bool
}
func (a *Agent) newSensorConfig() *SensorConfig {
@@ -33,25 +40,29 @@ func (a *Agent) newSensorConfig() *SensorConfig {
sysSensors, _ := utils.GetEnv("SYS_SENSORS")
sensorsEnvVal, sensorsSet := utils.GetEnv("SENSORS")
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)
var (
errTemperatureFetchTimeout = errors.New("temperature collection timed out")
temperatureFetchTimeout = 2 * time.Second
)
// newSensorConfigWithEnv creates a SensorConfig with the provided environment variables
// 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{
context: context.Background(),
primarySensor: primarySensor,
timeout: timeout,
skipCollection: skipCollection,
firstRun: true,
sensors: make(map[string]struct{}),
}
@@ -167,6 +178,14 @@ func (a *Agent) getTempsWithTimeout(getTemps getTempsFn) ([]sensors.TemperatureS
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)
@@ -176,7 +195,7 @@ func (a *Agent) getTempsWithTimeout(getTemps getTempsFn) ([]sensors.TemperatureS
select {
case res := <-resultCh:
return res.temps, res.err
case <-time.After(temperatureFetchTimeout):
case <-time.After(timeout):
return nil, errTemperatureFetchTimeout
}
}

View File

@@ -168,6 +168,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
primarySensor string
sysSensors string
sensors string
sensorsTimeout string
skipCollection bool
expectedConfig *SensorConfig
}{
@@ -179,12 +180,37 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "",
timeout: 2 * time.Second,
sensors: map[string]struct{}{},
isBlacklist: false,
hasWildcards: 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",
primarySensor: "",
@@ -194,6 +220,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "",
timeout: 2 * time.Second,
sensors: map[string]struct{}{},
isBlacklist: false,
hasWildcards: false,
@@ -208,6 +235,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{},
isBlacklist: false,
hasWildcards: false,
@@ -221,6 +249,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{
"cpu_temp": {},
"gpu_temp": {},
@@ -237,6 +266,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{
"cpu_temp": {},
"gpu_temp": {},
@@ -253,6 +283,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{
"cpu_*": {},
"gpu_temp": {},
@@ -269,6 +300,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
expectedConfig: &SensorConfig{
context: context.Background(),
primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{
"cpu_*": {},
"gpu_temp": {},
@@ -284,6 +316,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
sensors: "cpu_temp",
expectedConfig: &SensorConfig{
primarySensor: "cpu_temp",
timeout: 2 * time.Second,
sensors: map[string]struct{}{
"cpu_temp": {},
},
@@ -295,7 +328,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
for _, tt := range tests {
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
assert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor)
@@ -314,6 +347,7 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
// Check flags
assert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist)
assert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards)
assert.Equal(t, tt.expectedConfig.timeout, result.timeout)
// Check context
if tt.sysSensors != "" {
@@ -333,12 +367,14 @@ func TestNewSensorConfig(t *testing.T) {
t.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary")
t.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path")
t.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3")
t.Setenv("BESZEL_AGENT_SENSORS_TIMEOUT", "7s")
agent := &Agent{}
result := agent.newSensorConfig()
// Verify results
assert.Equal(t, "test_primary", result.primarySensor)
assert.Equal(t, 7*time.Second, result.timeout)
assert.NotNil(t, result.sensors)
assert.Equal(t, 3, len(result.sensors))
assert.True(t, result.hasWildcards)
@@ -532,15 +568,10 @@ func TestGetTempsWithTimeout(t *testing.T) {
agent := &Agent{
sensorConfig: &SensorConfig{
context: context.Background(),
timeout: 10 * time.Millisecond,
},
}
originalTimeout := temperatureFetchTimeout
t.Cleanup(func() {
temperatureFetchTimeout = originalTimeout
})
temperatureFetchTimeout = 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
@@ -567,15 +598,13 @@ func TestUpdateTemperaturesSkipsOnTimeout(t *testing.T) {
systemInfo: system.Info{DashboardTemp: 99},
sensorConfig: &SensorConfig{
context: context.Background(),
timeout: 10 * time.Millisecond,
},
}
originalTimeout := temperatureFetchTimeout
t.Cleanup(func() {
temperatureFetchTimeout = originalTimeout
getSensorTemps = sensors.TemperaturesWithContext
})
temperatureFetchTimeout = 10 * time.Millisecond
getSensorTemps = func(ctx context.Context) ([]sensors.TemperatureStat, error) {
time.Sleep(50 * time.Millisecond)
return nil, nil

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

View File

@@ -25,12 +25,15 @@ import (
// SmartManager manages data collection for SMART devices
type SmartManager struct {
sync.Mutex
SmartDataMap map[string]*smart.SmartData
SmartDevices []*DeviceInfo
refreshMutex sync.Mutex
lastScanTime time.Time
smartctlPath string
excludedDevices map[string]struct{}
SmartDataMap map[string]*smart.SmartData
SmartDevices []*DeviceInfo
refreshMutex sync.Mutex
lastScanTime time.Time
smartctlPath string
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 {
@@ -1033,6 +1036,52 @@ func parseScsiGigabytesProcessed(value string) int64 {
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
// Returns hasValidData and exitStatus
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.FirmwareVersion = data.FirmwareVersion
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.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
smartData.DiskName = data.Device.Name

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"
"runtime"
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery"
@@ -23,13 +22,6 @@ import (
"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
func (a *Agent) refreshSystemDetails() {
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
func (a *Agent) getSystemStats(cacheTimeMs uint16) 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

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

9
go.mod
View File

@@ -5,7 +5,6 @@ go 1.26.1
require (
github.com/blang/semver v3.5.1+incompatible
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/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8
@@ -13,8 +12,8 @@ require (
github.com/lxzan/gws v1.9.1
github.com/nicholas-fedor/shoutrrr v0.14.1
github.com/pocketbase/dbx v1.12.0
github.com/pocketbase/pocketbase v0.36.7
github.com/shirou/gopsutil/v4 v4.26.2
github.com/pocketbase/pocketbase v0.36.8
github.com/shirou/gopsutil/v4 v4.26.3
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
@@ -23,6 +22,7 @@ require (
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/sys v0.42.0
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.1
)
require (
@@ -61,9 +61,8 @@ require (
golang.org/x/sync v0.20.0 // indirect
golang.org/x/term v0.41.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/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.2 // indirect
modernc.org/sqlite v1.48.0 // indirect
)

14
go.sum
View File

@@ -17,8 +17,6 @@ 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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
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/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -98,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/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.36.7 h1:MrViB7BptPYrf2Nt25pJEYBqUdFjuhRKu1p5GTrkvPA=
github.com/pocketbase/pocketbase v0.36.7/go.mod h1:qX4HuVjoKXtEg41fSJVM0JLfGWXbBmHxVv/FaE446r4=
github.com/pocketbase/pocketbase v0.36.8 h1:gCNqoesZ44saYOD3J7edhi5nDwUWKyQG7boM/kVwz2c=
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/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -107,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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
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/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@@ -199,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/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.2 h1:gkXQ6R0+AjxFC/fTDaeIVLbNLNrRoOK7YYVz5BKhTcE=
modernc.org/sqlite v1.46.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
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/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -109,6 +109,18 @@ func (am *AlertManager) cancelPendingAlert(alertID string) bool {
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.
func (am *AlertManager) processPendingAlert(alertID string) {
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")
})
}
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]
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..]
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.
@@ -93,10 +95,12 @@ type FsStats struct {
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
MaxDiskReadBytes uint64 `json:"rbm,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 {

View File

@@ -3,6 +3,7 @@ package hub
import (
"context"
"net/http"
"regexp"
"strings"
"time"
@@ -25,6 +26,32 @@ type UpdateInfo struct {
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
func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
// authorizes request with user matching the provided email
@@ -33,7 +60,7 @@ func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
return e.Next()
}
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 {
return e.Next()
}
@@ -84,19 +111,19 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
// send test notification
apiAuth.POST("/test-notification", h.SendTestNotification)
// heartbeat status and test
apiAuth.GET("/heartbeat-status", h.getHeartbeatStatus)
apiAuth.POST("/test-heartbeat", h.testHeartbeat)
apiAuth.GET("/heartbeat-status", h.getHeartbeatStatus).BindFunc(requireAdminRole)
apiAuth.POST("/test-heartbeat", h.testHeartbeat).BindFunc(requireAdminRole)
// get config.yml content
apiAuth.GET("/config-yaml", config.GetYamlConfig)
apiAuth.GET("/config-yaml", config.GetYamlConfig).BindFunc(requireAdminRole)
// handle agent websocket connection
apiNoAuth.GET("/agent-connect", h.handleAgentConnect)
// get or create universal tokens
apiAuth.GET("/universal-token", h.getUniversalToken)
apiAuth.GET("/universal-token", h.getUniversalToken).BindFunc(excludeReadOnlyRole)
// update / delete user alerts
apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts)
apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts)
// refresh SMART devices for a system
apiAuth.POST("/smart/refresh", h.refreshSmartData)
apiAuth.POST("/smart/refresh", h.refreshSmartData).BindFunc(excludeReadOnlyRole)
// get systemd service details
apiAuth.GET("/systemd/info", h.getSystemdInfo)
// /containers routes
@@ -153,6 +180,10 @@ func (info *UpdateInfo) getUpdate(e *core.RequestEvent) error {
// GetUniversalToken handles the universal token API endpoint (create, read, delete)
func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
if e.Auth.IsSuperuser() {
return e.ForbiddenError("Superusers cannot use universal tokens", nil)
}
tokenMap := universalTokenMap.GetMap()
userID := e.Auth.Id
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
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 {
return e.JSON(http.StatusOK, map[string]any{
"enabled": false,
@@ -266,9 +294,6 @@ func (h *Hub) getHeartbeatStatus(e *core.RequestEvent) error {
// testHeartbeat triggers a single heartbeat ping and returns the result
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 {
return e.JSON(http.StatusOK, map[string]any{
"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")
containerID := e.Request.URL.Query().Get("container")
if systemID == "" || containerID == "" {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and container parameters are required"})
}
if !containerIDPattern.MatchString(containerID) {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "invalid container parameter"})
if systemID == "" || containerID == "" || !containerIDPattern.MatchString(containerID) {
return e.BadRequestError("Invalid system or container parameter", nil)
}
system, err := h.sm.GetSystem(systemID)
if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
if err != nil || !system.HasUser(e.App, e.Auth.Id) {
return e.NotFoundError("", nil)
}
data, err := fetchFunc(system, containerID)
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})
@@ -325,15 +347,23 @@ func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
serviceName := query.Get("service")
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)
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 {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
return e.NotFoundError("", err)
}
details, err := system.FetchSystemdInfoFromAgent(serviceName)
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")
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 {
systemID := e.Request.URL.Query().Get("system")
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)
if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
if err != nil || !system.HasUser(e.App, e.Auth.Id) {
return e.NotFoundError("", nil)
}
// Fetch and save SMART devices
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"})

View File

@@ -3,6 +3,7 @@ package hub_test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
@@ -25,33 +26,33 @@ func jsonReader(v any) io.Reader {
}
func TestApiRoutesAuthentication(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
hub, user := beszelTests.GetHubWithUser(t)
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()
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{
"name": "test-system",
"users": []string{user.Id},
@@ -106,7 +107,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 403,
ExpectedContent: []string{"Requires admin"},
ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory,
},
{
@@ -136,7 +137,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 403,
ExpectedContent: []string{"Requires admin role"},
ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory,
},
{
@@ -158,7 +159,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 403,
ExpectedContent: []string{"Requires admin role"},
ExpectedContent: []string{"The authorized record is not allowed to perform this action."},
TestAppFactory: testAppFactory,
},
{
@@ -202,6 +203,74 @@ func TestApiRoutesAuthentication(t *testing.T) {
ExpectedContent: []string{"\"permanent\":true", "permanent-token-123"},
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",
Method: http.MethodPost,
@@ -273,20 +342,42 @@ func TestApiRoutesAuthentication(t *testing.T) {
{
Name: "GET /containers/logs - no auth should fail",
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,
ExpectedContent: []string{"requires valid"},
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",
Method: http.MethodGet,
URL: "/api/beszel/containers/logs?container=test-container",
URL: "/api/beszel/containers/logs?container=abababababab",
Headers: map[string]string{
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"system and container parameters are required"},
ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory,
},
{
@@ -297,7 +388,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"system and container parameters are required"},
ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory,
},
{
@@ -308,7 +399,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 404,
ExpectedContent: []string{"system not found"},
ExpectedContent: []string{"The requested resource wasn't found."},
TestAppFactory: testAppFactory,
},
{
@@ -319,7 +410,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"invalid container parameter"},
ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory,
},
{
@@ -330,7 +421,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"invalid container parameter"},
ExpectedContent: []string{"Invalid", "parameter"},
TestAppFactory: testAppFactory,
},
{
@@ -341,9 +432,114 @@ func TestApiRoutesAuthentication(t *testing.T) {
"Authorization": userToken,
},
ExpectedStatus: 400,
ExpectedContent: []string{"invalid container parameter"},
ExpectedContent: []string{"Invalid", "parameter"},
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
{
@@ -434,13 +630,17 @@ func TestApiRoutesAuthentication(t *testing.T) {
"systems": []string{system.Id},
}),
},
{
Name: "GET /update - shouldn't exist without CHECK_UPDATES env var",
Method: http.MethodGet,
URL: "/api/beszel/update",
ExpectedStatus: 502,
TestAppFactory: testAppFactory,
},
// this works but diff behavior on prod vs dev.
// dev returns 502; prod returns 200 with static html page 404
// TODO: align dev and prod behavior and re-enable this test
// {
// Name: "GET /update - shouldn't exist without CHECK_UPDATES env var",
// Method: http.MethodGet,
// URL: "/api/beszel/update",
// NotExpectedContent: []string{"v:", "\"v\":"},
// ExpectedStatus: 502,
// TestAppFactory: testAppFactory,
// },
}
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
func GetYamlConfig(e *core.RequestEvent) error {
if e.Auth.GetString("role") != "admin" {
return e.ForbiddenError("Requires admin role", nil)
}
configContent, err := generateYAML(e.App)
if err != nil {
return err

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import (
"hash/fnv"
"math/rand"
"net"
"slices"
"strings"
"sync/atomic"
"time"
@@ -145,6 +146,7 @@ func (sys *System) update() error {
// update smart interval if it's set on the agent side
if data.Details.SmartInterval > 0 {
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
// to prevent premature expiration leading to new fetch if interval is different.
sys.manager.smartFetchMap.UpdateExpiration(sys.Id, sys.smartInterval+time.Minute)
@@ -156,11 +158,10 @@ func (sys *System) update() error {
if sys.smartInterval <= 0 {
sys.smartInterval = time.Hour
}
lastFetch, _ := sys.manager.smartFetchMap.GetOk(sys.Id)
if time.Since(time.UnixMilli(lastFetch-1e4)) >= sys.smartInterval && sys.smartFetching.CompareAndSwap(false, true) {
if sys.shouldFetchSmart() && sys.smartFetching.CompareAndSwap(false, true) {
sys.manager.hub.Logger().Info("SMART fetch", "system", sys.Id, "interval", sys.smartInterval.String())
go func() {
defer sys.smartFetching.Store(false)
sys.manager.smartFetchMap.Set(sys.Id, time.Now().UnixMilli(), sys.smartInterval+time.Minute)
_ = sys.FetchAndSaveSmartDevices()
}()
}
@@ -184,7 +185,7 @@ func (sys *System) handlePaused() {
// createRecords updates the system record and adds system_stats and container_stats records
func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {
systemRecord, err := sys.getRecord()
systemRecord, err := sys.getRecord(sys.manager.hub)
if err != nil {
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.
// If the record is not found, it removes the system from the manager.
func (sys *System) getRecord() (*core.Record, error) {
record, err := sys.manager.hub.FindRecordById("systems", sys.Id)
func (sys *System) getRecord(app core.App) (*core.Record, error) {
record, err := app.FindRecordById("systems", sys.Id)
if err != nil || record == nil {
_ = sys.manager.RemoveSystem(sys.Id)
return nil, err
@@ -352,6 +353,16 @@ func (sys *System) getRecord() (*core.Record, error) {
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.
// 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.
@@ -359,7 +370,7 @@ func (sys *System) setDown(originalError error) error {
if sys.Status == down || sys.Status == paused {
return nil
}
record, err := sys.getRecord()
record, err := sys.getRecord(sys.manager.hub)
if err != nil {
return err
}
@@ -643,6 +654,7 @@ func (s *System) createSSHClient() error {
return err
}
s.agentVersion, _ = extractAgentVersion(string(s.client.Conn.ServerVersion()))
s.manager.resetFailedSmartFetchState(s.Id)
return nil
}

View File

@@ -41,10 +41,10 @@ var errSystemExists = errors.New("system exists")
// SystemManager manages a collection of monitored systems and their connections.
// It handles system lifecycle, status updates, and maintains both SSH and WebSocket connections.
type SystemManager struct {
hub hubLike // Hub interface for database and alert operations
systems *store.Store[string, *System] // Thread-safe store of active systems
sshConfig *ssh.ClientConfig // SSH client configuration for system connections
smartFetchMap *expirymap.ExpiryMap[int64] // Stores last SMART fetch time per system ID
hub hubLike // Hub interface for database and alert operations
systems *store.Store[string, *System] // Thread-safe store of active systems
sshConfig *ssh.ClientConfig // SSH client configuration for system connections
smartFetchMap *expirymap.ExpiryMap[smartFetchState] // Stores last SMART fetch time/result; TTL is only for cleanup
}
// hubLike defines the interface requirements for the hub dependency.
@@ -54,6 +54,7 @@ type hubLike interface {
GetSSHKey(dataDir string) (ssh.Signer, error)
HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error
HandleStatusAlerts(status string, systemRecord *core.Record) error
CancelPendingStatusAlerts(systemID string)
}
// NewSystemManager creates a new SystemManager instance with the provided hub.
@@ -62,7 +63,7 @@ func NewSystemManager(hub hubLike) *SystemManager {
return &SystemManager{
systems: store.New(map[string]*System{}),
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()
}
_ = deactivateAlerts(e.App, e.Record.Id)
sm.hub.CancelPendingStatusAlerts(e.Record.Id)
return e.Next()
case pending:
// Resume monitoring, preferring existing WebSocket connection
@@ -306,6 +308,7 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
if err != nil {
return err
}
sm.resetFailedSmartFetchState(systemId)
system := sm.NewSystem(systemId)
system.WsConn = wsConn
@@ -317,6 +320,15 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
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
func (sm *SystemManager) createSSHClientConfig() error {
privateKey, err := sm.hub.GetSSHKey("")

View File

@@ -4,18 +4,61 @@ import (
"database/sql"
"errors"
"strings"
"time"
"github.com/henrygd/beszel/internal/entities/smart"
"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
func (sys *System) FetchAndSaveSmartDevices() error {
smartData, err := sys.FetchSmartDataFromAgent()
if err != nil || len(smartData) == 0 {
if err != nil {
sys.recordSmartFetchResult(err, 0)
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

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

@@ -230,6 +230,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Bandwidth[1] += stats.Bandwidth[1]
sum.DiskIO[0] += stats.DiskIO[0]
sum.DiskIO[1] += stats.DiskIO[1]
for i := range stats.DiskIoStats {
sum.DiskIoStats[i] += stats.DiskIoStats[i]
}
batterySum += int(stats.Battery[0])
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.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])
for i := range stats.DiskIoStats {
sum.MaxDiskIoStats[i] = max(sum.MaxDiskIoStats[i], stats.MaxDiskIoStats[i], stats.DiskIoStats[i])
}
// Accumulate network interfaces
if sum.NetworkInterfaces == nil {
@@ -299,6 +305,10 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
fs.DiskWriteBytes += value.DiskWriteBytes
fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)
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.DiskIO[0] = sum.DiskIO[0] / 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.NetworkRecv = twoDecimals(sum.NetworkRecv / 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.DiskReadBytes = fs.DiskReadBytes / 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",
"shiki": "^3.13.0",
"tailwind-merge": "^3.3.1",
"valibot": "^0.42.1",
"valibot": "^1.3.1",
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
@@ -927,7 +927,7 @@
"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=="],

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<link rel="manifest" href="./static/manifest.json" crossorigin="use-credentials" />
<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="robots" content="noindex, nofollow" />
<title>Beszel</title>

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,13 @@ import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
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 { $router, Link } from "@/components/router"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
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 { Switch } from "@/components/ui/switch"
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 }) {
const alerts = useStore($alerts)
const systems = useStore($systems)
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
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()
// 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
// current alerts, it will only be updated when first checked, then won't be updated because
// after that it exists.
@@ -93,18 +141,37 @@ export const AlertDialogContent = memo(function AlertDialogContent({ system }: {
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="system" onValueChange={setCurrentTab}>
<TabsList className="mb-1 -mt-0.5">
<TabsTrigger value="system">
<ServerIcon className="me-2 h-3.5 w-3.5" />
<span className="truncate max-w-60">{system.name}</span>
</TabsTrigger>
<TabsTrigger value="global">
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
<Trans>All Systems</Trans>
</TabsTrigger>
</TabsList>
<div className="flex items-center justify-between mb-1 -mt-0.5">
<TabsList>
<TabsTrigger value="system">
<ServerIcon className="me-2 h-3.5 w-3.5" />
<span className="truncate max-w-60">{system.name}</span>
</TabsTrigger>
<TabsTrigger value="global">
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
<Trans>All Systems</Trans>
</TabsTrigger>
</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">
<div className="grid gap-3">
<div key={copyKey} className="grid gap-3">
{alertKeys.map((name) => (
<AlertContent
key={name}

View File

@@ -41,8 +41,8 @@ export default function AreaChartDefault({
hideYAxis = false,
filter,
truncate = false,
}: // logRender = false,
{
chartProps,
}: {
chartData: ChartData
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
customData?: any[]
@@ -62,13 +62,13 @@ export default function AreaChartDefault({
hideYAxis?: boolean
filter?: string
truncate?: boolean
// logRender?: boolean
chartProps?: Omit<React.ComponentProps<typeof AreaChart>, "data" | "margin">
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
const sourceData = customData ?? chartData.systemStats
// Only update the rendered data while the chart is visible
const [displayData, setDisplayData] = useState(sourceData)
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
// Reduce chart redraws by only updating while visible or when chart time changes
useEffect(() => {
@@ -78,7 +78,10 @@ export default function AreaChartDefault({
if (shouldUpdate) {
setDisplayData(sourceData)
}
}, [displayData, isIntersecting, 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")
@@ -106,14 +109,14 @@ export default function AreaChartDefault({
/>
)
})
}, [areasKey, maxToggled])
}, [areasKey, displayMaxToggled])
return useMemo(() => {
if (displayData.length === 0) {
return null
}
// if (logRender) {
// console.log("Rendered at", new Date(), "for", dataPoints?.at(0)?.label)
// console.log("Rendered", dataPoints?.map((d) => d.label).join(", "), new Date())
// }
return (
<ChartContainer
@@ -128,6 +131,7 @@ export default function AreaChartDefault({
accessibilityLayer
data={displayData}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
{...chartProps}
>
<CartesianGrid vertical={false} />
{!hideYAxis && (
@@ -163,5 +167,5 @@ export default function AreaChartDefault({
</AreaChart>
</ChartContainer>
)
}, [displayData, yAxisWidth, showTotal, filter])
}, [displayData, yAxisWidth, filter, Areas])
}

View File

@@ -40,8 +40,8 @@ export default function LineChartDefault({
hideYAxis = false,
filter,
truncate = false,
}: // logRender = false,
{
chartProps,
}: {
chartData: ChartData
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
customData?: any[]
@@ -61,13 +61,13 @@ export default function LineChartDefault({
hideYAxis?: boolean
filter?: string
truncate?: boolean
// logRender?: boolean
chartProps?: Omit<React.ComponentProps<typeof LineChart>, "data" | "margin">
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
const sourceData = customData ?? chartData.systemStats
// Only update the rendered data while the chart is visible
const [displayData, setDisplayData] = useState(sourceData)
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
// Reduce chart redraws by only updating while visible or when chart time changes
useEffect(() => {
@@ -77,7 +77,10 @@ export default function LineChartDefault({
if (shouldUpdate) {
setDisplayData(sourceData)
}
}, [displayData, isIntersecting, 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 linesKey = dataPoints?.map((d) => `${d.label}:${d.strokeOpacity ?? ""}`).join("\0")
@@ -105,14 +108,14 @@ export default function LineChartDefault({
/>
)
})
}, [linesKey, maxToggled])
}, [linesKey, displayMaxToggled])
return useMemo(() => {
if (displayData.length === 0) {
return null
}
// if (logRender) {
// console.log("Rendered at", new Date(), "for", dataPoints?.at(0)?.label)
// console.log("Rendered", dataPoints?.map((d) => d.label).join(", "), new Date())
// }
return (
<ChartContainer
@@ -127,6 +130,7 @@ export default function LineChartDefault({
accessibilityLayer
data={displayData}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
{...chartProps}
>
<CartesianGrid vertical={false} />
{!hideYAxis && (
@@ -162,5 +166,5 @@ export default function LineChartDefault({
</LineChart>
</ChartContainer>
)
}, [displayData, yAxisWidth, showTotal, filter, chartData.chartTime])
}, [displayData, yAxisWidth, filter, Lines])
}

View File

@@ -63,7 +63,7 @@ export default function Navbar() {
className="p-2 ps-0 me-3 group"
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>
<Button
variant="outline"
@@ -125,15 +125,17 @@ export default function Navbar() {
<DropdownMenuSubContent>{AdminLinks}</DropdownMenuSubContent>
</DropdownMenuSub>
)}
<DropdownMenuItem
className="flex items-center"
onSelect={() => {
setAddSystemDialogOpen(true)
}}
>
<PlusIcon className="h-4 w-4 me-2.5" />
<Trans>Add {{ foo: systemTranslation }}</Trans>
</DropdownMenuItem>
{!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>
@@ -217,10 +219,12 @@ export default function Navbar() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<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>
{!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>
)

View File

@@ -1,18 +1,16 @@
import { memo, useState } from "react"
import { Trans } from "@lingui/react/macro"
import { compareSemVer, parseSemVer } from "@/lib/utils"
import type { GPUData } from "@/types"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import InfoBar from "./system/info-bar"
import { useSystemData } from "./system/use-system-data"
import { CpuChart, ContainerCpuChart } from "./system/charts/cpu-charts"
import { MemoryChart, ContainerMemoryChart, SwapChart } from "./system/charts/memory-charts"
import { DiskCharts } from "./system/charts/disk-charts"
import { RootDiskCharts, ExtraFsCharts } from "./system/charts/disk-charts"
import { BandwidthChart, ContainerNetworkChart } from "./system/charts/network-charts"
import { TemperatureChart, BatteryChart } from "./system/charts/sensor-charts"
import { GpuPowerChart, GpuDetailCharts } from "./system/charts/gpu-charts"
import { ExtraFsCharts } from "./system/charts/extra-fs-charts"
import { LazyContainersTable, LazySmartTable, LazySystemdTable } from "./system/lazy-tables"
import { LoadAverageChart } from "./system/charts/load-average-chart"
import { ContainerIcon, CpuIcon, HardDriveIcon, TerminalSquareIcon } from "lucide-react"
@@ -24,6 +22,8 @@ const SEMVER_0_14_0 = parseSemVer("0.14.0")
const SEMVER_0_15_0 = parseSemVer("0.15.0")
export default memo(function SystemDetail({ id }: { id: string }) {
const systemData = useSystemData(id)
const {
system,
systemStats,
@@ -48,7 +48,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
hasGpuData,
hasGpuEnginesData,
hasGpuPowerData,
} = useSystemData(id)
} = systemData
// extra margin to add to bottom of page, specifically for temperature chart,
// where the tooltip can go past the bottom of the page if lots of sensors
@@ -103,7 +103,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
/>
)}
<DiskCharts {...coreProps} systemStats={systemStats} />
<RootDiskCharts systemData={systemData} />
<BandwidthChart {...coreProps} systemStats={systemStats} />
@@ -138,7 +138,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
/>
)}
<ExtraFsCharts {...coreProps} systemStats={systemStats} />
<ExtraFsCharts systemData={systemData} />
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
@@ -188,6 +188,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<LoadAverageChart chartData={chartData} grid={grid} dataEmpty={dataEmpty} />
<BandwidthChart {...coreProps} systemStats={systemStats} />
<TemperatureChart {...coreProps} setPageBottomExtraMargin={setPageBottomExtraMargin} />
<BatteryChart {...coreProps} />
<SwapChart chartData={chartData} grid={grid} dataEmpty={dataEmpty} systemStats={systemStats} />
{pageBottomExtraMargin > 0 && <div style={{ marginBottom: pageBottomExtraMargin }}></div>}
</div>
@@ -197,9 +198,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{mountedTabs.has("disk") && (
<>
<div className="grid xl:grid-cols-2 gap-4">
<DiskCharts {...coreProps} systemStats={systemStats} />
<RootDiskCharts systemData={systemData} />
</div>
<ExtraFsCharts {...coreProps} systemStats={systemStats} />
<ExtraFsCharts systemData={systemData} />
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
</>
)}

View File

@@ -1,106 +1,283 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
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"
export function DiskCharts({
chartData,
grid,
dataEmpty,
showMax,
isLongerChart,
maxValues,
}: {
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()
// 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)
}
return (
<>
<ChartCard empty={dataEmpty} grid={grid} title={t`Disk Usage`} description={t`Usage of root partition`}>
<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: ({ stats }) => stats?.du,
},
]}
></AreaChartDefault>
</ChartCard>
const title = extraFsName ? `${extraFsName} ${t`Usage`}` : t`Disk Usage`
const description = extraFsName ? t`Disk usage of ${extraFsName}` : t`Usage of root partition`
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Disk I/O`}
description={t`Throughput of root filesystem`}
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }: SystemStatsRecord) => {
if (showMax) {
return stats?.dio?.[1] ?? (stats?.dwm ?? 0) * 1024 * 1024
}
return stats?.dio?.[1] ?? (stats?.dw ?? 0) * 1024 * 1024
},
color: 3,
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }: SystemStatsRecord) => {
if (showMax) {
return stats?.diom?.[0] ?? (stats?.drm ?? 0) * 1024 * 1024
}
return stats?.dio?.[0] ?? (stats?.dr ?? 0) * 1024 * 1024
},
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}`
}}
showTotal={true}
/>
</ChartCard>
</>
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

@@ -1,120 +0,0 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { ChartCard, SelectAvgMax } from "../chart-card"
import { Unit } from "@/lib/enums"
export function ExtraFsCharts({
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()
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">
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} ${t`Usage`}`}
description={t`Disk usage of ${extraFsName}`}
>
<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: ({ stats }) => stats?.efs?.[extraFsName]?.du,
},
]}
></AreaChartDefault>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} I/O`}
description={t`Throughput of ${extraFsName}`}
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
showTotal={true}
dataPoints={[
{
label: t`Write`,
dataKey: ({ stats }) => {
if (showMax) {
return stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
}
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
},
color: 3,
opacity: 0.3,
},
{
label: t`Read`,
dataKey: ({ stats }) => {
if (showMax) {
return stats?.efs?.[extraFsName]?.rbm ?? (stats?.efs?.[extraFsName]?.rm ?? 0) * 1024 * 1024
}
return stats?.efs?.[extraFsName]?.rb ?? (stats?.efs?.[extraFsName]?.r ?? 0) * 1024 * 1024
},
color: 1,
opacity: 0.3,
},
]}
maxToggled={showMax}
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>
</div>
)
})}
</div>
)
}

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

@@ -36,7 +36,7 @@ import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { pb } from "@/lib/api"
import { isReadOnlyUser, pb } from "@/lib/api"
import type { SmartDeviceRecord, SmartAttribute } from "@/types"
import {
formatBytes,
@@ -492,7 +492,7 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
const tableColumns = useMemo(() => {
const columns = createColumns(longestName, longestModel, longestDevice)
const baseColumns = systemId ? columns.filter((col) => col.id !== "system") : columns
return [...baseColumns, actionColumn]
return isReadOnlyUser() ? baseColumns : [...baseColumns, actionColumn]
}, [systemId, actionColumn, longestName, longestModel, longestDevice])
const table = useReactTable({

View File

@@ -28,6 +28,8 @@ import type {
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)
@@ -190,7 +192,7 @@ export function useSystemData(id: string) {
// 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) {
if (lastCreated && Date.now() - lastCreated < expectedInterval * 0.9) {
return
}
} else {

View File

@@ -18,7 +18,7 @@ import { listenKeys } from "nanostores"
import { memo, type ReactNode, useEffect, useMemo, useRef, useState } from "react"
import { getStatusColor, systemdTableCols } from "@/components/systemd-table/systemd-table-columns"
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 { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
@@ -161,13 +161,13 @@ export default function SystemdTable({ systemId }: { systemId?: string }) {
<CardTitle className="mb-2">
<Trans>Systemd Services</Trans>
</CardTitle>
<CardDescription className="flex items-center">
<div className="text-sm text-muted-foreground flex items-center flex-wrap">
<Trans>Total: {data.length}</Trans>
<Separator orientation="vertical" className="h-4 mx-2 bg-primary/40" />
<Trans>Failed: {statusTotals[ServiceStatus.Failed]}</Trans>
<Separator orientation="vertical" className="h-4 mx-2 bg-primary/40" />
<Trans>Updated every 10 minutes.</Trans>
</CardDescription>
</div>
</div>
<Input
placeholder={t`Filter...`}

View File

@@ -460,14 +460,14 @@ const SystemCard = memo(
}
)}
>
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
<div className="flex items-center gap-2 w-full overflow-hidden">
<CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5">
<CardHeader className="py-1 ps-4 pe-2 bg-muted/30 border-b border-border/60">
<div className="flex items-center gap-1 w-full overflow-hidden">
<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">
<IndicatorDot system={system} />
<span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span>
</div>
</CardTitle>
</h3>
{table.getColumn("actions")?.getIsVisible() && (
<div className="flex gap-1 shrink-0 relative z-10">
<AlertButton system={system} />

View File

@@ -43,7 +43,7 @@ const AlertDialogContent = React.forwardRef<
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
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"

View File

@@ -18,11 +18,7 @@ CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-[1.4em] sm: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"

View File

@@ -52,7 +52,7 @@ const DialogContent = React.forwardRef<
DialogContent.displayName = DialogPrimitive.Content.displayName
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"

View File

@@ -177,6 +177,10 @@
}
}
@utility text-card-title {
@apply text-[1.4rem] sm:text-2xl;
}
.recharts-tooltip-wrapper {
z-index: 51;
@apply tabular-nums;

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \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"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} متاح"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "المتوسط ينخفض أقل من <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "المتوسط يتجاوز <0>{value}{0}</0>"
#: 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"
msgstr "متوسط ​​استهلاك طاقة وحدة معالجة الرسوميات"
#: 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"
msgstr "متوسط استخدام وحدة المعالجة المركزية على مستوى النظام"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "نسخ المضيف"
@@ -476,7 +490,7 @@ msgstr "نسخ YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "النواة"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "يوميًا"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "افتراضي"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "وحدة القرص"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "استخدام القرص"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "استخدام القرص لـ {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "عرض"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "طريقة HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "تنسيق الحمولة"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "النسبة المئوية للوقت المقضي في كل حالة"
@@ -1259,7 +1291,7 @@ msgstr "المنفذ"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "المنافذ"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "تم بدء العملية"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "ساعات الهدوء"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "قراءة"
@@ -1549,7 +1588,7 @@ msgstr "جدول"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "تبويبات"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "لا يمكن التراجع عن هذا الإجراء. سيؤدي ذل
msgid "This will permanently delete all selected records from the database."
msgstr "سيؤدي هذا إلى حذف جميع السجلات المحددة من قاعدة البيانات بشكل دائم."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "معدل نقل {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "إجمالي البيانات المستلمة لكل واجهة"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "رفع"
msgid "Uptime"
msgstr "مدة التشغيل"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "مستخدم"
msgid "Users"
msgstr "المستخدمون"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "الاستخدام"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "القيمة"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "عرض"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "عرض المزيد"
@@ -1858,7 +1908,9 @@ msgstr "أمر ويندوز"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "كتابة"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-03-28 09:32\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "Версия {0} е налична"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Средната стойност пада под <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Средната стойност надхвърля <0>{value}{0}</0>"
#: 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"
msgstr "Средна консумация на ток от графични карти"
#: 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"
msgstr "Средно използване на процесора на цялата система"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "Копирай хоста"
@@ -476,7 +490,7 @@ msgstr "Копирай YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Основни"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Дневно"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Подредба"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Единица за диск"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Използване на диск"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Изполване на диск от {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Показване"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP метод"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Формат на полезния товар"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Процент време, прекарано във всяко състояние"
@@ -1259,7 +1291,7 @@ msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Портове"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Процесът стартира"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Тихи часове"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Прочети"
@@ -1549,7 +1588,7 @@ msgstr "Таблица"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Табове"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Това действие не може да бъде отменено.
msgid "This will permanently delete all selected records from the database."
msgstr "Това ще доведе до трайно изтриване на всички избрани записи от базата данни."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Пропускателна способност на {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Общо получени данни за всеки интерфейс"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Качване"
msgid "Uptime"
msgstr "Време на работа"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Използвани"
msgid "Users"
msgstr "Потребители"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Натоварване"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Стойност"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Изглед"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Виж повече"
@@ -1858,7 +1908,9 @@ msgstr "Команда Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Запиши"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} k dispozici"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Průměr klesne pod <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Průměr je vyšší než <0>{value}{0}</0>"
#: 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"
msgstr "Průměrná spotřeba energie GPU"
#: 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"
msgstr "Průměrné využití CPU v celém systému"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Kopírovat hostitele"
@@ -476,7 +490,7 @@ msgstr "Kopírovat YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Jádro"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Denně"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Výchozí"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Disková jednotka"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Využití disku"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Využití disku {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Zobrazení"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP metoda"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Formát payloadu"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Procento času strávěného v každém stavu"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Porty"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proces spuštěn"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Tiché hodiny"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Číst"
@@ -1549,7 +1588,7 @@ msgstr "Tabulka"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Karty"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ 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."
msgstr "Tímto trvale odstraníte všechny vybrané záznamy z databáze."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Propustnost {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Celkový přijatý objem dat pro každé rozhraní"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Odeslání"
msgid "Uptime"
msgstr "Doba provozu"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Využito"
msgid "Users"
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
msgid "Value"
msgstr "Hodnota"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Zobrazení"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Zobrazit více"
@@ -1858,7 +1908,9 @@ msgstr "Windows příkaz"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Psát"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} tilgængelig"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Gennemsnit falder under <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Gennemsnittet overstiger <0>{value}{0}</0>"
#: 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"
msgstr "Gennemsnitligt strømforbrug for GPU'er"
#: 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"
msgstr "Gennemsnitlig systembaseret CPU-udnyttelse"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "Kopier vært"
@@ -476,7 +490,7 @@ msgstr "Kopier YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Kerne"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Dagligt"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Standard"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Diskenhed"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Diskforbrug"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Diskforbrug af {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Visning"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -870,7 +883,7 @@ msgstr "Sundhed"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr ""
msgstr "Hjerteslag"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -898,6 +911,21 @@ msgstr "HTTP-metode"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Payload-format"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Procentdel af tid brugt i hver tilstand"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Porte"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proces startet"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Stille timer"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Læs"
@@ -1549,7 +1588,7 @@ msgstr "Tabel"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Faner"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Denne handling kan ikke fortrydes. Dette vil permanent slette alle aktue
msgid "This will permanently delete all selected records from the database."
msgstr "Dette vil permanent slette alle poster fra databasen."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Gennemløb af {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Samlet modtaget data for hver interface"
msgid "Total data sent for each 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Overfør"
msgid "Uptime"
msgstr "Oppetid"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Brugt"
msgid "Users"
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
msgid "Value"
msgstr "Værdi"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Vis"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Se mere"
@@ -1858,7 +1908,9 @@ msgstr "Windows-kommando"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Skriv"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-14 20:37\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} verfügbar"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Durchschnitt unterschreitet <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Durchschnitt überschreitet <0>{value}{0}</0>"
#: 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"
msgstr "Durchschnittlicher Stromverbrauch der GPUs"
#: 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"
msgstr "Durchschnittliche systemweite CPU-Auslastung"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "Host kopieren"
@@ -476,7 +490,7 @@ msgstr "YAML kopieren"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Kern"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Täglich"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Standard"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Festplatteneinheit"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Festplattennutzung"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Festplattennutzung von {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Anzeige"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP-Methode"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Payload-Format"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Prozentsatz der Zeit in jedem Zustand"
@@ -1284,13 +1316,20 @@ msgstr "Prozess gestartet"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Ruhezeiten"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Lesen"
@@ -1602,7 +1641,7 @@ msgstr "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch werden alle
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."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Durchsatz von {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Empfangene Gesamtdatenmenge je Schnittstelle "
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Hochladen"
msgid "Uptime"
msgstr "Betriebszeit"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Verwendet"
msgid "Users"
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
msgid "Value"
msgstr "Wert"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Ansicht"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Mehr anzeigen"
@@ -1858,7 +1908,9 @@ msgstr "Windows-Befehl"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Schreiben"

View File

@@ -204,10 +204,19 @@ msgstr "Average drops below <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Average exceeds <0>{value}{0}</0>"
#: 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"
msgstr "Average power consumption of GPUs"
#: 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"
msgstr "Average system-wide CPU utilization"
@@ -439,6 +448,11 @@ msgctxt "Environment variables"
msgid "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
msgid "Copy host"
msgstr "Copy host"
@@ -594,12 +608,11 @@ msgstr "Disk unit"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Disk Usage"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Disk usage of {extraFsName}"
@@ -893,6 +906,21 @@ msgstr "HTTP Method"
msgid "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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1202,6 +1230,10 @@ msgstr "Payload format"
msgid "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
msgid "Percentage of time spent in each state"
msgstr "Percentage of time spent in each state"
@@ -1279,13 +1311,20 @@ msgstr "Process started"
msgid "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
msgid "Quiet Hours"
msgstr "Quiet Hours"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Read"
@@ -1597,7 +1636,7 @@ msgstr "This action cannot be undone. This will permanently delete all current r
msgid "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/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Throughput of {extraFsName}"
@@ -1650,6 +1689,11 @@ msgstr "Total data received for each interface"
msgid "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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1770,7 +1814,7 @@ msgstr "Upload"
msgid "Uptime"
msgstr "Uptime"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1792,6 +1836,11 @@ msgstr "Used"
msgid "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
msgid "Value"
msgstr "Value"
@@ -1801,6 +1850,7 @@ msgid "View"
msgstr "View"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "View more"
@@ -1853,7 +1903,9 @@ msgstr "Windows command"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Write"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} disponible"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "El promedio cae por debajo de <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "El promedio excede <0>{value}{0}</0>"
#: 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"
msgstr "Consumo de energía promedio de GPUs"
#: 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"
msgstr "Utilización promedio de CPU del sistema"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Copiar host"
@@ -476,7 +490,7 @@ msgstr "Copiar YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Núcleo"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Diariamente"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Predeterminado"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Unidad de disco"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Uso de disco"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Uso de disco de {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Pantalla"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -870,7 +883,7 @@ msgstr "Estado"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr ""
msgstr "Latido"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -898,6 +911,21 @@ msgstr "Método HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Formato de carga útil (payload)"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Porcentaje de tiempo dedicado a cada estado"
@@ -1259,7 +1291,7 @@ msgstr "Puerto"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Puertos"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proceso iniciado"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Horas de silencio"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Lectura"
@@ -1549,7 +1588,7 @@ msgstr "Tabla"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Pestañas"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Esta acción no se puede deshacer. Esto eliminará permanentemente todos
msgid "This will permanently delete all selected records from the database."
msgstr "Esto eliminará permanentemente todos los registros seleccionados de la base de datos."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Rendimiento de {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Datos totales recibidos por cada interfaz"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Cargar"
msgid "Uptime"
msgstr "Tiempo de actividad"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Usado"
msgid "Users"
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
msgid "Value"
msgstr "Valor"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Vista"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Ver más"
@@ -1858,7 +1908,9 @@ msgstr "Comando Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Escritura"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} در دسترس است"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "میانگین به زیر <0>{value}{0}</0> می‌افتد"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "میانگین از <0>{value}{0}</0> فراتر رفته است"
#: 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"
msgstr "میانگین مصرف برق پردازنده‌های گرافیکی"
#: 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"
msgstr "میانگین استفاده از CPU در کل سیستم"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "کپی میزبان"
@@ -476,7 +490,7 @@ msgstr "کپی YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "هسته"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "روزانه"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "پیش‌فرض"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "واحد دیسک"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "میزان استفاده از دیسک"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "میزان استفاده از دیسک {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "نمایش"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "متد HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "فرمت پی‌لود"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "درصد زمان صرف شده در هر حالت"
@@ -1259,7 +1291,7 @@ msgstr "پورت"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "پورت‌ها"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "فرآیند شروع شد"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "ساعات آرام"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "خواندن"
@@ -1549,7 +1588,7 @@ msgstr "جدول"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "تب‌ها"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "این عمل قابل برگشت نیست. این کار تمام رک
msgid "This will permanently delete all selected records from the database."
msgstr "این کار تمام رکوردهای انتخاب شده را برای همیشه از پایگاه داده حذف خواهد کرد."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "توان عملیاتی {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "داده‌های کل دریافت شده برای هر رابط"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "آپلود"
msgid "Uptime"
msgstr "آپتایم"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "استفاده شده"
msgid "Users"
msgstr "کاربران"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "بهره‌وری"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "مقدار"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "مشاهده"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "مشاهده بیشتر"
@@ -1858,7 +1908,9 @@ msgstr "دستور Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "نوشتن"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} disponible"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "La moyenne descend en dessous de <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "La moyenne dépasse <0>{value}{0}</0>"
#: 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"
msgstr "Consommation d'énergie moyenne des GPUs"
#: 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"
msgstr "Utilisation moyenne du CPU à l'échelle du système"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Copier l'hôte"
@@ -476,7 +490,7 @@ msgstr "Copier YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Cœur"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Quotidien"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Par défaut"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Unité disque"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Utilisation du disque"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Utilisation du disque de {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Affichage"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -870,7 +883,7 @@ msgstr "Santé"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr ""
msgstr "Pulsation"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -898,6 +911,21 @@ msgstr "Méthode HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Format de la charge utile"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Pourcentage de temps passé dans chaque état"
@@ -1284,13 +1316,20 @@ msgstr "Processus démarré"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Heures calmes"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Lecture"
@@ -1549,7 +1588,7 @@ msgstr "Tableau"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Onglets"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Cette action ne peut pas être annulée. Cela supprimera définitivement
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."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Débit de {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Données totales reçues pour chaque interface"
msgid "Total data sent for each 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Téléverser"
msgid "Uptime"
msgstr "Temps de fonctionnement"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Utilisé"
msgid "Users"
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
msgid "Value"
msgstr "Valeur"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Vue"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Voir plus"
@@ -1858,7 +1908,9 @@ msgstr "Commande Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Écriture"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: he\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \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"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} זמין"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "הממוצע יורד מתחת ל-<0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "הממוצע עולה על <0>{value}{0}</0>"
#: 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"
msgstr "צריכת חשמל ממוצעת של GPUs"
#: 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"
msgstr "ניצול ממוצע כלל-מערכתי של CPU"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "העתק מארח"
@@ -476,13 +490,13 @@ msgstr "העתק YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "ליבה"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr "CPU"
msgstr ""
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
@@ -550,7 +564,7 @@ msgstr "יומי"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "ברירת מחדל"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "יחידת דיסק"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "שימוש בדיסק"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "שימוש בדיסק של {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "תצוגה"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "שיטת HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "פורמט מטען (Payload)"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "אחוז הזמן המוקדש לכל מצב"
@@ -1259,7 +1291,7 @@ msgstr "פורט"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "פורטים"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "תהליך התחיל"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "שעות שקט"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "קריאה"
@@ -1549,7 +1588,7 @@ msgstr "טבלה"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "לשוניות"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "פעולה זו לא ניתנת לביטול. פעולה זו תמחק
msgid "This will permanently delete all selected records from the database."
msgstr "פעולה זו תמחק לצמיתות את כל הרשומות שנבחרו ממסד הנתונים."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "תפוקה של {extraFsName}"
@@ -1626,7 +1665,7 @@ msgstr "החלף ערכת נושא"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1655,6 +1694,11 @@ msgstr "סך נתונים שהתקבלו עבור כל ממשק"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "העלאה"
msgid "Uptime"
msgstr "זמן פעילות"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "בשימוש"
msgid "Users"
msgstr "משתמשים"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "ניצולת"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "ערך"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "צפה"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "צפה בעוד"
@@ -1858,7 +1908,9 @@ msgstr "פקודת Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "כתיבה"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} dostupno"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Prosjek pada ispod <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Prosjek premašuje <0>{value}{0}</0>"
#: 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"
msgstr "Prosječna potrošnja energije grafičkog procesora"
#: 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"
msgstr "Prosječna iskorištenost procesora u cijelom sustavu"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Kopiraj hosta"
@@ -476,7 +490,7 @@ msgstr "Kopiraj YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Jezgra"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Dnevno"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Zadano"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Mjerna jedinica za disk"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Iskorištenost Diska"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Iskorištenost diska od {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Prikaz"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP metoda"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Format korisnog tereta (Payload)"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Postotak vremena provedenog u svakom stanju"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Portovi"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proces pokrenut"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Tihi sati"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Pročitaj"
@@ -1549,7 +1588,7 @@ msgstr "Tablica"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Kartice"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Ova radnja ne može se poništiti. Svi trenutni zapisi za {name} bit će
msgid "This will permanently delete all selected records from the database."
msgstr "Ovom radnjom će se trajno izbrisati svi odabrani zapisi iz baze podataka."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Protok {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Ukupni podaci primljeni za svako sučelje"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Otpremi"
msgid "Uptime"
msgstr "Vrijeme rada"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Iskorišteno"
msgid "Users"
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
msgid "Value"
msgstr "Vrijednost"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Prikaz"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Prikaži više"
@@ -1858,7 +1908,9 @@ msgstr "Windows naredba"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Piši"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} elérhető"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Az átlag esik <0>{value}{0}</0> alá"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Az átlag meghaladja a <0>{value}{0}</0> értéket"
#: 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"
msgstr "GPU-k átlagos energiafogyasztása"
#: 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"
msgstr "Rendszerszintű CPU átlagos kihasználtság"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "Hoszt másolása"
@@ -476,7 +490,7 @@ msgstr "YAML másolása"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Mag"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Napi"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Alapértelmezett"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Lemez mértékegysége"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Lemezhasználat"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Lemezhasználat a {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Megjelenítés"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP metódus"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Payload formátum"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Az idő százalékos aránya minden állapotban"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Portok"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Folyamat elindítva"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Csendes órák"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Olvasás"
@@ -1549,7 +1588,7 @@ msgstr "Tábla"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Lapok"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ 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."
msgstr "Ez véglegesen törli az összes kijelölt bejegyzést az adatbázisból."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "A {extraFsName} átviteli teljesítménye"
@@ -1655,6 +1694,11 @@ msgstr "Összes fogadott adat minden interfészenként"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Feltöltés"
msgid "Uptime"
msgstr "Üzemidő"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Felhasznált"
msgid "Users"
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
msgid "Value"
msgstr "Érték"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Nézet"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Továbbiak megjelenítése"
@@ -1858,7 +1908,9 @@ msgstr "Windows parancs"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Írás"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: id\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n"
"Language-Team: Indonesian\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} tersedia"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Rata-rata turun di bawah <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Rata-rata melebihi <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Jumlah rata-rata operasi I/O yang menunggu untuk dilayani"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Rata-rata konsumsi daya GPU"
#: 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 "Waktu rata-rata antrian hingga selesai per operasi"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Rata-rata utilisasi CPU seluruh sistem"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Salin env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Salin dari"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Salin host"
@@ -476,7 +490,7 @@ msgstr "Salin YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Inti"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -599,19 +613,18 @@ msgstr "Unit disk"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Penggunaan Disk"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Penggunaan disk dari {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Tampilan"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "Metode HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Metode HTTP: POST, GET, atau 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 ""
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "Waktu 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 "Utilisasi I/O"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -910,7 +938,7 @@ msgstr "Jika anda kehilangan kata sandi untuk akun admin anda, anda dapat merese
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Docker image"
msgid "Image"
msgstr ""
msgstr "Gambar"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
@@ -1207,6 +1235,10 @@ msgstr "Format payload"
msgid "Per-core average utilization"
msgstr "Rata-rata utilisasi per-inti"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Persentase waktu disk sibuk dengan I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Persentase waktu yang dihabiskan di setiap status"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Port"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proses dimulai"
msgid "Public Key"
msgstr "Kunci Publik"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Kedalaman Antrian"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Jam Tenang"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Baca"
@@ -1549,7 +1588,7 @@ msgstr "Tabel"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Tab"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Aksi ini tidak dapat di kembalikan. ini akan menghapus permanen semua re
msgid "This will permanently delete all selected records from the database."
msgstr "Ini akan menghapus secara permanen semua record yang dipilih dari database."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Throughput dari {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Total data yang diterima untuk setiap antarmuka"
msgid "Total data sent for each interface"
msgstr "Total data yang dikirim untuk setiap antarmuka"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Unggah"
msgid "Uptime"
msgstr "Waktu aktif"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Digunakan"
msgid "Users"
msgstr "Pengguna"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utilisasi"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Nilai"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Lihat"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Lihat lebih banyak"
@@ -1858,7 +1908,9 @@ msgstr "Perintah Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Tulis"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} disponibile"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "La media scende sotto <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "La media supera <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Numero medio di operazioni di I/O in attesa di essere servite"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Consumo energetico medio delle GPU"
#: 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 "Tempo medio dalla coda al completamento per operazione"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Utilizzo medio della CPU a livello di sistema"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Copia env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Copia da"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Copia host"
@@ -550,7 +564,7 @@ msgstr "Giornaliero"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Predefinito"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,12 +613,11 @@ msgstr "Unità disco"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Utilizzo Disco"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Utilizzo del disco di {extraFsName}"
@@ -898,6 +911,21 @@ msgstr "Metodo HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Metodo HTTP: POST, GET o HEAD (predefinito: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "Attesa 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 "Tempo 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 "Utilizzo I/O"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Formato del payload"
msgid "Per-core average utilization"
msgstr "Utilizzo medio per core"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Percentuale di tempo in cui il disco è occupato con l'I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Percentuale di tempo trascorso in ogni stato"
@@ -1259,7 +1291,7 @@ msgstr "Porta"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Porte"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Processo avviato"
msgid "Public Key"
msgstr "Chiave Pub"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Profondità coda"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Ore silenziose"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Lettura"
@@ -1549,7 +1588,7 @@ msgstr "Tabella"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Schede"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Questa azione non può essere annullata. Questo eliminerà permanentemen
msgid "This will permanently delete all selected records from the database."
msgstr "Questo eliminerà permanentemente tutti i record selezionati dal database."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Throughput di {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Dati totali ricevuti per ogni interfaccia"
msgid "Total data sent for each interface"
msgstr "Dati totali inviati per ogni interfaccia"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Carica"
msgid "Uptime"
msgstr "Tempo di attività"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Utilizzato"
msgid "Users"
msgstr "Utenti"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utilizzo"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Valore"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Vista"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Visualizza altro"
@@ -1858,7 +1908,9 @@ msgstr "Comando Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Scrittura"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} が利用可能"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "平均が<0>{value}{0}</0>を下回っています"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "平均が<0>{value}{0}</0>を超えています"
#: 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"
msgstr "GPUの平均消費電力"
#: 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"
msgstr "システム全体の平均CPU使用率"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "ホストをコピー"
@@ -476,7 +490,7 @@ msgstr "YAMLをコピー"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "コア"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "毎日"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "デフォルト"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "ディスク単位"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "ディスク使用率"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "{extraFsName}のディスク使用率"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "表示"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP メソッド"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "ペイロード形式"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "各状態で費やした時間の割合"
@@ -1259,7 +1291,7 @@ msgstr "ポート"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "ポート"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "プロセス開始"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "サイレント時間"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "読み取り"
@@ -1549,7 +1588,7 @@ msgstr "テーブル"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "タブ"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "この操作は元に戻せません。これにより、データベー
msgid "This will permanently delete all selected records from the database."
msgstr "これにより、選択したすべてのレコードがデータベースから完全に削除されます。"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "{extraFsName}のスループット"
@@ -1655,6 +1694,11 @@ msgstr "各インターフェースの総受信データ量"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "アップロード"
msgid "Uptime"
msgstr "稼働時間"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "使用中"
msgid "Users"
msgstr "ユーザー"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "利用率"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "値"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "表示"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "もっと見る"
@@ -1858,7 +1908,9 @@ msgstr "Windows コマンド"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "書き込み"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} 사용 가능"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "평균이 <0>{value}{0}</0> 아래로 떨어집니다"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "평균이 <0>{value}{0}</0>을(를) 초과합니다"
#: 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"
msgstr "GPU들의 평균 전원 사용량"
#: 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"
msgstr "시스템 전체의 평균 CPU 사용량"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "호스트 복사"
@@ -476,7 +490,7 @@ msgstr "YAML 복사"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "코어"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "매일"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "기본값"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "디스크 단위"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "디스크 사용량"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "{extraFsName}의 디스크 사용량"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "표시"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP 메서드"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "페이로드 형식"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "각 상태에서 보낸 시간의 백분율"
@@ -1259,7 +1291,7 @@ msgstr "포트"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "포트"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "프로세스 시작됨"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "조용한 시간"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "읽기"
@@ -1549,7 +1588,7 @@ msgstr "표"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "이 작업은 되돌릴 수 없습니다. 데이터베이스에서 {name
msgid "This will permanently delete all selected records from the database."
msgstr "선택한 모든 레코드를 데이터베이스에서 영구적으로 삭제합니다."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "{extraFsName}의 처리량"
@@ -1655,6 +1694,11 @@ msgstr "각 인터페이스별 총합 다운로드 데이터량"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "업로드"
msgid "Uptime"
msgstr "가동 시간"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "사용됨"
msgid "Users"
msgstr "사용자"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "사용률"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "값"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "보기"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "더 보기"
@@ -1858,7 +1908,9 @@ msgstr "Windows 명령어"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "쓰기"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} beschikbaar"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Gemiddelde daalt onder <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Gemiddelde overschrijdt <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Gemiddeld aantal I/O-bewerkingen dat wacht op afhandeling"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Gemiddeld stroomverbruik van GPU's"
#: 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 "Gemiddelde tijd van wachtrij tot voltooiing per bewerking"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Gemiddeld systeembrede CPU-gebruik"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Env kopiëren"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopiëren van"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Kopieer host"
@@ -476,7 +490,7 @@ msgstr "YAML kopiëren"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Kern"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Dagelijks"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Standaard"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Schijf eenheid"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Schijfgebruik"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Schijfgebruik van {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Weergave"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP-methode"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP-methode: POST, GET of HEAD (standaard: 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 Wachten"
#: 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 Tijd"
#: 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 Gebruik"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Payload-indeling"
msgid "Per-core average utilization"
msgstr "Gemiddeld gebruik per kern"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Percentage van de tijd dat de schijf bezig is met I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Percentage tijd besteed in elke status"
@@ -1259,7 +1291,7 @@ msgstr "Poort"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Poorten"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proces gestart"
msgid "Public Key"
msgstr "Publieke sleutel"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Wachtrijdiepte"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Stille uren"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Lezen"
@@ -1549,7 +1588,7 @@ msgstr "Tabel"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Tabbladen"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Deze actie kan niet ongedaan worden gemaakt. Dit zal alle huidige record
msgid "This will permanently delete all selected records from the database."
msgstr "Dit zal alle geselecteerde records verwijderen uit de database."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Doorvoer van {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Totaal ontvangen gegevens per interface"
msgid "Total data sent for each interface"
msgstr "Totaal verzonden gegevens per 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Uploaden"
msgid "Uptime"
msgstr "Actief"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Gebruikt"
msgid "Users"
msgstr "Gebruikers"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Gebruik"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Waarde"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Weergave"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Meer weergeven"
@@ -1858,7 +1908,9 @@ msgstr "Windows-commando"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Schrijven"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-03-31 07:42\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} tilgjengelig"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Gjennomsnittet faller under <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Gjennomsnittet overstiger <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Gjennomsnittlig antall I/O-operasjoner som venter på behandling"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Gjennomsnittlig strømforbruk for GPU-er"
#: 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 "Gjennomsnittlig tid fra kø til ferdigstillelse per operasjon"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Gjennomsnittlig CPU-utnyttelse for hele systemet"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Kopier env"
#: 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
msgid "Copy host"
msgstr "Kopier vert"
@@ -476,7 +490,7 @@ msgstr "Kopier YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Kjerne"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Daglig"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Standard"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Diskenhet"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Diskbruk"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Diskbruk av {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Vis"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -870,7 +883,7 @@ msgstr "Helse"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr ""
msgstr "Hjerteslag"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -898,6 +911,21 @@ msgstr "HTTP-metode"
msgid "HTTP method: POST, GET, or HEAD (default: 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-ventetid"
#: 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-utnyttelse"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Nyttelastformat"
msgid "Per-core average utilization"
msgstr "Gjennomsnittlig utnyttelse per kjerne"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Prosentandel av tiden disken er opptatt med I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Prosentandel av tid brukt i hver tilstand"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Porter"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Prosess startet"
msgid "Public Key"
msgstr "Offentlig Nøkkel"
#: 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
msgid "Quiet Hours"
msgstr "Stille timer"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Lesing"
@@ -1549,7 +1588,7 @@ msgstr "Tabell"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Faner"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Denne handlingen kan ikke omgjøres. Dette vil slette alle poster for {n
msgid "This will permanently delete all selected records from the database."
msgstr "Dette vil permanent slette alle valgte oppføringer fra databasen."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Gjennomstrømning av {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Totalt mottatt data for hvert grensesnitt"
msgid "Total data sent for each interface"
msgstr "Totalt sendt data for hvert grensesnitt"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Last opp"
msgid "Uptime"
msgstr "Oppetid"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Brukt"
msgid "Users"
msgstr "Brukere"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utnyttelse"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Verdi"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Visning"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Se mer"
@@ -1858,7 +1908,9 @@ msgstr "Windows-kommando"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Skriving"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} jest dostępna"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -40,7 +40,7 @@ msgstr "{count, plural, one {{countString} dzień} few {{countString} dni} many
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
msgstr "{count, plural, one {godzinę} few {{countString} godziny} many {{countString} godzin} other {{countString} godziny}}"
msgstr "{count, plural, one {godzina} few {{countString} godziny} many {{countString} godzin} other {{countString} godziny}}"
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
@@ -110,7 +110,7 @@ msgstr "Aktywne alerty"
#: src/components/systemd-table/systemd-table.tsx
msgid "Active state"
msgstr "Stan aktywny"
msgstr "Status aktywny"
#: src/components/add-system.tsx
#: src/components/add-system.tsx
@@ -145,7 +145,7 @@ msgstr "Po"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Po ustawieniu zmiennych środowiskowych zrestartuj hub Beszel, aby zmiany weszły w życie."
msgstr "Po ustawieniu zmiennych środowiskowych zrestartuj Beszel hub, aby zmiany zostały zaaplikowane."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
@@ -209,10 +209,19 @@ msgstr "Średnia spada poniżej <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Średnia przekracza <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Średnia liczba operacji I/O oczekujących na obsłużenie"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Średnie zużycie energii przez GPU"
#: 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 "Średni czas od kolejki do zakończenia operacji"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Średnie wykorzystanie CPU w całym systemie"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Kopiuj env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopiuj z"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Kopiuj host"
@@ -476,7 +490,7 @@ msgstr "Kopiuj YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Główne"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Codziennie"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Domyślne"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Jednostka dysku"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Użycie dysku"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Wykorzystanie dysku {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Widok"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "Metoda HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Metoda HTTP: POST, GET lub HEAD (domyślnie: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "Oczekiwanie 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 "Czas 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 "Użycie I/O"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1200,13 +1228,17 @@ msgstr "Wstrzymane ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Format ładunku"
msgstr "Format payload'u"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
msgstr "Średnie wykorzystanie na rdzeń"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Procent czasu, w którym dysk jest zajęty operacjami I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Procent czasu spędzonego w każdym stanie"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Porty"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proces uruchomiony"
msgid "Public Key"
msgstr "Klucz publiczny"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Głębokość kolejki"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Godziny ciszy"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Odczyt"
@@ -1422,7 +1461,7 @@ msgstr "Wyślij pojedynczy ping heartbeat, aby sprawdzić, czy punkt końcowy dz
#: 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."
msgstr "Wysyłaj okresowe pingi wychodzące do zewnętrznej usługi monitorowania, dzięki czemu możesz monitorować Beszel bez wystawiania go na działanie Internetu."
msgstr "Wysyłaj cyklicznie pingi wychodzące do zewnętrznej usługi monitorowania, co pozwala monitorować Beszel bez potrzeby udostępniania go publicznie w sieci."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
@@ -1549,7 +1588,7 @@ msgstr "Tabela"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Karty"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1588,7 +1627,7 @@ msgstr "Testowe powiadomienie wysłane."
#: 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."
msgstr "Ogólny status to <0>ok</0>, gdy wszystkie systemy działają, <1>ostrzeżenie</1>, gdy wyzwalane są alerty, oraz <2>błąd</2>, gdy którykolwiek system nie działa."
msgstr "Ogólny status to <0>ok</0>, gdy wszystkie systemy działają, <1>ostrzeżenie</1>, gdy wyzwalane są alerty, oraz <2>błąd</2>, gdy którykolwiek z systemów nie działa."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
@@ -1602,7 +1641,7 @@ msgstr "Tej akcji nie można cofnąć. Spowoduje to trwałe usunięcie wszystkic
msgid "This will permanently delete all selected records from the database."
msgstr "Spowoduje to trwałe usunięcie wszystkich zaznaczonych rekordów z bazy danych."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Przepustowość {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Całkowita ilość danych odebranych dla każdego interfejsu"
msgid "Total data sent for each interface"
msgstr "Całkowita ilość danych wysłanych dla każdego interfejsu"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Wysyłanie"
msgid "Uptime"
msgstr "Czas pracy"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Używane"
msgid "Users"
msgstr "Użytkownicy"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Użycie"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Wartość"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Widok"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Zobacz więcej"
@@ -1848,7 +1898,7 @@ msgstr "Gdy jest włączony, ten token pozwala agentom na samodzielną rejestrac
#: 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."
msgstr "W przypadku korzystania z POST każdy heartbeat zawiera ładunek JSON z podsumowaniem statusu systemu, listą wyłączonych systemów i wyzwolonymi alertami."
msgstr "Przy użyciu metody POST każdy heartbeat zawiera payload JSON z podsumowaniem statusu systemu, listą niedostępnych systemów i wywołanymi alertami."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -1858,7 +1908,9 @@ msgstr "Polecenie Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Zapis"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} disponível"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "A média cai abaixo de <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "A média excede <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Número médio de operações de E/S à espera de serem atendidas"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Consumo médio de energia pelas GPU's"
#: 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 "Tempo médio de fila até à conclusão por operação"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Utilização média de CPU em todo o sistema"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Copiar variáveis de ambiente"
#: 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
msgid "Copy host"
msgstr "Copiar host"
@@ -550,7 +564,7 @@ msgstr "Diariamente"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Predefinido"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Unidade de disco"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Uso de Disco"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Uso de disco de {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Ecrã"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "Método HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Método HTTP: POST, GET ou HEAD (predefinido: 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 "Tempo 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 "Utilização de E/S"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Formato do payload"
msgid "Per-core average utilization"
msgstr "Utilização média por núcleo"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Percentagem de tempo em que o disco está ocupado com E/S"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Percentagem de tempo gasto em cada estado"
@@ -1259,7 +1291,7 @@ msgstr "Porta"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Portas"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Processo iniciado"
msgid "Public Key"
msgstr "Chave Pública"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Profundidade da fila"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Horas Silenciosas"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Ler"
@@ -1549,7 +1588,7 @@ msgstr "Tabela"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Separadores"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Esta ação não pode ser desfeita. Isso excluirá permanentemente todos
msgid "This will permanently delete all selected records from the database."
msgstr "Isso excluirá permanentemente todos os registros selecionados do banco de dados."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Taxa de transferência de {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Dados totais recebidos para cada interface"
msgid "Total data sent for each interface"
msgstr "Dados totais enviados para cada 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Carregar"
msgid "Uptime"
msgstr "Tempo de Atividade"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Usado"
msgid "Users"
msgstr "Usuários"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Utilização"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Valor"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Visual"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Ver mais"
@@ -1858,7 +1908,9 @@ msgstr "Comando Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Escrever"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-21 09:46\n"
"PO-Revision-Date: 2026-03-27 22:12\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} доступно"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Среднее опускается ниже <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Среднее превышает <0>{value}{0}</0>"
#: 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"
msgstr "Среднее потребление мощности всеми GPU"
#: 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"
msgstr "Среднее использование CPU по всей системе"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Копировать хост"
@@ -476,7 +490,7 @@ msgstr "Скопировать YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Процессор"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Ежедневно"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "По умолчанию"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Единицы измерения дисковой активности"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Использование диска"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Использование диска {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Отображение"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP-метод"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Формат полезной нагрузки"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Процент времени, проведенного в каждом состоянии"
@@ -1259,7 +1291,7 @@ msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Порты"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Процесс запущен"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Тихие часы"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Чтение"
@@ -1549,7 +1588,7 @@ msgstr "Таблица"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Вкладки"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Это действие не может быть отменено. Эт
msgid "This will permanently delete all selected records from the database."
msgstr "Это навсегда удалит все выбранные записи из базы данных."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Пропускная способность {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Общий объем полученных данных для кажд
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Отдача"
msgid "Uptime"
msgstr "Время работы"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Использовано"
msgid "Users"
msgstr "Пользователи"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Использование"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Значение"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Вид"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Показать больше"
@@ -1858,7 +1908,9 @@ msgstr "Команда Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Запись"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} na voljo"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Povprečje pade pod <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Povprečje presega <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Povprečno število I/O operacij, ki čakajo na obdelavo"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Povprečna poraba energije GPU"
#: 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 "Povprečni čas od čakalne vrste do zaključka na operacijo"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Povprečna CPU izkoriščenost v celotnem sistemu"
@@ -239,7 +248,7 @@ msgstr "Pasovna širina"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Baterija"
#: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Kopiraj okoljske spremenljivke"
#: 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
msgid "Copy host"
msgstr "Kopiraj gostitelja"
@@ -476,7 +490,7 @@ msgstr "Kopiraj YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Jedro"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Dnevno"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Privzeto"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Enota diska"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Poraba diska"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Poraba diska za {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Zaslon"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "Metoda HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Metoda HTTP: POST, GET ali HEAD (privzeto: 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 čakanje"
#: 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 izkoriščenost"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Oblika tovora (payload)"
msgid "Per-core average utilization"
msgstr "Povprečna izkoriščenost na jedro"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Odstotek časa, ko je disk zaposlen z I/O operacijami"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Odstotek časa, preživetega v vsakem stanju"
@@ -1259,7 +1291,7 @@ msgstr "Vrata"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Vrata"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Proces začet"
msgid "Public Key"
msgstr "Javni ključ"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Globina čakalne vrste"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Tihi čas"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Preberano"
@@ -1549,7 +1588,7 @@ msgstr "Tabela"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Zavihki"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Tega dejanja ni mogoče razveljaviti. To bo trajno izbrisalo vse trenutn
msgid "This will permanently delete all selected records from the database."
msgstr "To bo trajno izbrisalo vse izbrane zapise iz zbirke podatkov."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Prepustnost {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Skupni prejeti podatki za vsak vmesnik"
msgid "Total data sent for each interface"
msgstr "Skupni poslani podatki za vsak vmesnik"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Naloži"
msgid "Uptime"
msgstr "Čas delovanja"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Uporabljeno"
msgid "Users"
msgstr "Uporabniki"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Izkoriščenost"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Vrednost"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Pogled"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Prikaži več"
@@ -1858,7 +1908,9 @@ msgstr "Ukaz Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Pisanje"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-02-23 15:11\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Serbian (Cyrillic)\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"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} доступно"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Prosek pada ispod <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Просек премашује <0>{value}{0}</0>"
#: 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"
msgstr "Просечна потрошња енергије графичких картица"
#: 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"
msgstr "Просечна системска искоришћеност процесор"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Копирај хоста"
@@ -476,7 +490,7 @@ msgstr "Копирај YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Језгро"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Дневно"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Подразумевано"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Диск јединица"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Употреба диска"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Употреба диска {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Приказ"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP metod"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metod: POST, GET ili HEAD (podrazumevano: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Format korisnog opterećenja (payload)"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Проценат времена проведеног у сваком стању"
@@ -1259,7 +1291,7 @@ msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Портови"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Процес покренут"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Тихи сати"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Читање"
@@ -1549,7 +1588,7 @@ msgstr "Табела"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Картице"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Ова акција се не може опозвати. Ово ће т
msgid "This will permanently delete all selected records from the database."
msgstr "Ово ће трајно избрисати све изабране записе из базе података."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Проток {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Укупни подаци примљени за сваки интерф
msgid "Total data sent for each interface"
msgstr "Укупни подаци poslati за сваки интерфејс"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Отпреми"
msgid "Uptime"
msgstr "Време рада"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Коришћено"
msgid "Users"
msgstr "Корисници"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Искоришћеност"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Вредност"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Погледај"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Погледај више"
@@ -1858,7 +1908,9 @@ msgstr "Windows команда"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Писање"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sv\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} tillgänglig"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Genomsnittet sjunker under <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Genomsnittet överskrider <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Genomsnittligt antal I/O-operationer som väntar på att betjänas"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Genomsnittlig strömförbrukning för GPU:er"
#: 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 "Genomsnittlig tid i kö till färdigställande per operation"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Genomsnittlig systemomfattande CPU-användning"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Kopiera env"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Kopiera från"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Kopiera värd"
@@ -476,7 +490,7 @@ msgstr "Kopiera YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Kärna"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Dagligen"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Standard"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Diskenhet"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Diskanvändning"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Diskanvändning av {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Visa"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -870,7 +883,7 @@ msgstr "Hälsa"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr ""
msgstr "Hjärtslag"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -898,6 +911,21 @@ msgstr "HTTP-metod"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP-metod: 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 Väntan"
#: 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 Användning"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Nyttolastformat"
msgid "Per-core average utilization"
msgstr "Genomsnittlig användning per kärna"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Procent av tiden disken är upptagen med I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Procentandel av tid spenderad i varje tillstånd"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Portar"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Process startad"
msgid "Public Key"
msgstr "Offentlig nyckel"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Kö-djup"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Tysta timmar"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Läs"
@@ -1549,7 +1588,7 @@ msgstr "Tabell"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Flikar"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Den här åtgärden kan inte ångras. Detta kommer permanent att ta bort
msgid "This will permanently delete all selected records from the database."
msgstr "Detta kommer permanent att ta bort alla valda poster från databasen."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Genomströmning av {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Totalt mottagen data för varje gränssnitt"
msgid "Total data sent for each interface"
msgstr "Totalt skickad data för varje gränssnitt"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Ladda upp"
msgid "Uptime"
msgstr "Drifttid"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Använt"
msgid "Users"
msgstr "Användare"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Användning"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Värde"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Visa"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Visa mer"
@@ -1858,7 +1908,9 @@ msgstr "Windows-kommando"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Skriv"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: tr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-02 13:16\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} mevcut"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Ortalama <0>{value}{0}</0> altına düşüyor"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Ortalama <0>{value}{0}</0> aşıyor"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Servis edilmeyi bekleyen ortalama G/Ç (I/O) işlemi sayısı"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "GPU ların ortalama güç tüketimi"
#: 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 "İşlem başına ortalama kuyruktan tamamlanmaya kadar geçen süre"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Sistem genelinde ortalama CPU kullanımı"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Ortam değişkenlerini kopyala"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Şuradan kopyala"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Ana bilgisayarı kopyala"
@@ -476,7 +490,7 @@ msgstr "YAML'ı kopyala"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Çekirdek"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Günlük"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Varsayılan"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Disk birimi"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Disk Kullanımı"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "{extraFsName} disk kullanımı"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Görünüm"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP Yöntemi"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP yöntemi: POST, GET veya HEAD (varsayılan: POST)"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average operation time (iostat await)"
msgid "I/O Await"
msgstr "G/Ç Bekleme"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "G/Ç Süresi"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Percent of time the disk is busy with I/O"
msgid "I/O Utilization"
msgstr "G/Ç Kullanımı"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Yük (Payload) formatı"
msgid "Per-core average utilization"
msgstr "Çekirdek başına ortalama kullanım"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Diskin G/Ç işlemleriyle meşgul olduğu sürenin yüzdesi"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Her durumda harcanan zamanın yüzdesi"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Portlar"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Süreç başlatıldı"
msgid "Public Key"
msgstr "Genel Anahtar"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Kuyruk Derinliği"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Sessiz Saatler"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Oku"
@@ -1549,7 +1588,7 @@ msgstr "Tablo"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Sekmeler"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Bu işlem geri alınamaz. Bu, veritabanından {name} için tüm mevcut k
msgid "This will permanently delete all selected records from the database."
msgstr "Bu, seçilen tüm kayıtları veritabanından kalıcı olarak silecektir."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "{extraFsName} verimliliği"
@@ -1655,6 +1694,11 @@ msgstr "Her arayüz için alınan toplam veri"
msgid "Total data sent for each interface"
msgstr "Her arayüz için gönderilen toplam veri"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Yükle"
msgid "Uptime"
msgstr "Çalışma Süresi"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Kullanıldı"
msgid "Users"
msgstr "Kullanıcılar"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Kullanım"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Değer"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Görüntüle"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Daha fazla göster"
@@ -1858,7 +1908,9 @@ msgstr "Windows komutu"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Yaz"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: uk\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} доступно"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Середнє опускається нижче <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Середнє перевищує <0>{value}{0}</0>"
#: 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"
msgstr "Середнє енергоспоживання GPUs"
#: 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"
msgstr "Середнє використання CPU по всій системі"
@@ -239,7 +248,7 @@ msgstr "Пропускна здатність"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Батарея"
#: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy 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
msgid "Copy host"
msgstr "Копіювати хост"
@@ -476,7 +490,7 @@ msgstr "Копіювати YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Ядро"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Щодня"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "За замовчуванням"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Одиниця виміру диска"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Використання диска"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Використання диска {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Відображення"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP-метод"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Формат корисного навантаження"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "Відсоток часу, проведеного в кожному стані"
@@ -1259,7 +1291,7 @@ msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Порти"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Процес запущено"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "Тихі години"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Читання"
@@ -1549,7 +1588,7 @@ msgstr "Таблиця"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Вкладки"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Цю дію не можна скасувати. Це назавжди в
msgid "This will permanently delete all selected records from the database."
msgstr "Це назавжди видалить усі вибрані записи з бази даних."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Пропускна здатність {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Загальний обсяг отриманих даних для ко
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Відвантажити"
msgid "Uptime"
msgstr "Час роботи"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Використано"
msgid "Users"
msgstr "Користувачі"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Використання"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Значення"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Вигляд"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Переглянути більше"
@@ -1858,7 +1908,9 @@ msgstr "Команда Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Запис"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: vi\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n"
"Language-Team: Vietnamese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} có sẵn"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "Trung bình giảm xuống dưới <0>{value}{0}</0>"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Trung bình vượt quá <0>{value}{0}</0>"
#: src/components/routes/system/disk-io-sheet.tsx
msgid "Average number of I/O operations waiting to be serviced"
msgstr "Số lượng trung bình các hoạt động I/O đang chờ được xử lý"
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "Average power consumption of GPUs"
msgstr "Tiêu thụ điện năng trung bình của GPU"
#: 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 "Thời gian trung bình từ khi xếp hàng đến khi hoàn thành mỗi hoạt động"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Average system-wide CPU utilization"
msgstr "Sử dụng CPU trung bình toàn hệ thống"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
msgstr "Sao chép môi trường"
#: src/components/alerts/alerts-sheet.tsx
msgctxt "Copy alerts from another system"
msgid "Copy from"
msgstr "Sao chép từ"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Sao chép máy chủ"
@@ -476,7 +490,7 @@ msgstr "Sao chép YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "Lõi"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "Hàng ngày"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "Mặc định"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "Đơn vị đĩa"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "Sử dụng Đĩa"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "Sử dụng đĩa của {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "Hiển thị"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "Phương thức HTTP"
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Phương thức HTTP: POST, GET hoặc HEAD (mặc định: 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 Chờ"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O total time spent on read/write"
msgid "I/O Time"
msgstr "Thời gian 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 "Sử dụng I/O"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "Định dạng tải trọng (payload)"
msgid "Per-core average utilization"
msgstr "Tỷ lệ sử dụng trung bình mỗi nhân"
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Percent of time the disk is busy with I/O"
msgstr "Phần trăm thời gian đĩa bận rộn với các hoạt động I/O"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Phần trăm thời gian dành cho mỗi trạng thái"
@@ -1259,7 +1291,7 @@ msgstr "Cổng"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Cổng"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "Tiến trình đã khởi động"
msgid "Public Key"
msgstr "Khóa"
#: src/components/routes/system/disk-io-sheet.tsx
msgctxt "Disk I/O average queue depth"
msgid "Queue Depth"
msgstr "Độ sâu hàng đợi"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Giờ yên tĩnh"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Đọc"
@@ -1549,7 +1588,7 @@ msgstr "Bảng"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "Tab"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "Hành động này không thể hoàn tác. Điều này sẽ xóa vĩnh
msgid "This will permanently delete all selected records from the database."
msgstr "Thao tác này sẽ xóa vĩnh viễn tất cả các bản ghi đã chọn khỏi cơ sở dữ liệu."
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "Thông lượng của {extraFsName}"
@@ -1655,6 +1694,11 @@ msgstr "Tổng dữ liệu nhận được cho mỗi giao diện"
msgid "Total data sent for each interface"
msgstr "Tổng dữ liệu gửi đi cho mỗi giao diện"
#: 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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "Tải lên"
msgid "Uptime"
msgstr "Thời gian hoạt động"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "Đã sử dụng"
msgid "Users"
msgstr "Người dùng"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "Sử dụng"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Giá trị"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "Xem"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Xem thêm"
@@ -1858,7 +1908,9 @@ msgstr "Lệnh Windows"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "Ghi"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-01 17:14\n"
"PO-Revision-Date: 2026-04-05 18:27\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} 可用"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "平均值降至<0>{value}{0}</0>以下"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "平均值超过<0>{value}{0}</0>"
#: 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"
msgstr "GPU 平均能耗"
#: 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"
msgstr "系统范围内的平均 CPU 使用率"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "复制主机名"
@@ -476,7 +490,7 @@ msgstr "复制 YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "核心"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "每日"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "默认"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "磁盘单位"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "磁盘使用"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "{extraFsName}的磁盘使用"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "显示"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP 方法"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "有效载荷 (Payload) 格式"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "在每个状态下花费的时间百分比"
@@ -1259,7 +1291,7 @@ msgstr "端口"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "端口"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "进程启动"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "静默时间"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "读取"
@@ -1549,7 +1588,7 @@ msgstr "表格"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "标签页"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "此操作无法撤销。这将永久删除数据库中{name}的所有当
msgid "This will permanently delete all selected records from the database."
msgstr "这将永久删除数据库中所有选定的记录。"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "{extraFsName}的吞吐量"
@@ -1655,6 +1694,11 @@ msgstr "每个接口的总接收数据量"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "上传"
msgid "Uptime"
msgstr "正常运行时间"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "已用"
msgid "Users"
msgstr "用户"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "利用率"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "值"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "视图"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "查看更多"
@@ -1858,7 +1908,9 @@ msgstr "Windows 安装命令"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "写入"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-04-05 18:28\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} 可用"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -209,10 +209,19 @@ msgstr "平均值降至<0>{value}{0}</0>以下"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "平均值超過 <0>{value}{0}</0>"
#: 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"
msgstr "GPU 的平均功耗"
#: 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"
msgstr "系統的平均 CPU 使用率"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "複製主機"
@@ -476,7 +490,7 @@ msgstr "複製YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "核心"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "每日"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "預設"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "磁碟單位"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "磁碟使用"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "{extraFsName} 的磁碟使用量"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "顯示"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -898,6 +911,21 @@ msgstr "HTTP 方法"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1207,6 +1235,10 @@ msgstr "負載 (Payload) 格式"
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "在每個狀態下花費的時間百分比"
@@ -1259,7 +1291,7 @@ msgstr "端口"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "連接埠"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "進程啟動"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "靜音時段"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "讀取"
@@ -1549,7 +1588,7 @@ msgstr "表格"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "分頁"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1602,7 +1641,7 @@ msgstr "此操作無法撤銷。這將永久刪除數據庫中{name}的所有當
msgid "This will permanently delete all selected records from the database."
msgstr "這將從資料庫中永久刪除所有選定的記錄。"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "{extraFsName}的吞吐量"
@@ -1655,6 +1694,11 @@ msgstr "每個介面的總接收資料量"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "上傳"
msgid "Uptime"
msgstr "正常運行時間"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "已用"
msgid "Users"
msgstr "用戶"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "利用率"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "值"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "檢視"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "查看更多"
@@ -1858,7 +1908,9 @@ msgstr "Windows 指令"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "寫入"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"PO-Revision-Date: 2026-03-28 02:52\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr ""
msgstr "{0} 現已推出"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -145,7 +145,7 @@ msgstr "之後"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "設環境變數後,重新啟動您的 Beszel hub 以使更生效。"
msgstr "設環境變數後,重新啟動 Beszel Hub 以使更生效。"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
@@ -209,10 +209,19 @@ msgstr "平均值降至<0>{value}{0}</0>以下"
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "平均值超過<0>{value}{0}</0>"
#: 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"
msgstr "GPU 的平均功耗"
#: 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"
msgstr "系統的平均 CPU 使用率"
@@ -444,6 +453,11 @@ msgctxt "Environment variables"
msgid "Copy env"
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
msgid "Copy host"
msgstr "複製主機"
@@ -476,7 +490,7 @@ msgstr "複製 YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr ""
msgstr "核心指標"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +564,7 @@ msgstr "每日"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr ""
msgstr "預設"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -599,19 +613,18 @@ msgstr "磁碟單位"
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/lib/alerts.ts
msgid "Disk Usage"
msgstr "磁碟使用量"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk usage of {extraFsName}"
msgstr "{extraFsName}的磁碟使用量"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr ""
msgstr "顯示"
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -682,11 +695,11 @@ msgstr "結束時間"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "端點 URL"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "要 ping 的端點 URL (必填)"
msgstr "要 Ping 的 Endpoint URL (必填)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
@@ -781,7 +794,7 @@ msgstr "儲存設定失敗"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "發送 heartbeat 失敗"
msgstr "Heartbeat 發送失敗"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
@@ -870,7 +883,7 @@ msgstr "健康狀態"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "心跳 (Heartbeat)"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -898,6 +911,21 @@ msgstr "HTTP 方法"
msgid "HTTP method: POST, GET, or HEAD (default: 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
#: src/lib/i18n.ts
msgid "Idle"
@@ -1200,13 +1228,17 @@ msgstr "已暫停 ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "有效載荷 (Payload) 格式"
msgstr "Payload 格式"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
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
msgid "Percentage of time spent in each state"
msgstr "狀態時間佔比"
@@ -1259,7 +1291,7 @@ msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr ""
msgstr "Port 映射"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1284,13 +1316,20 @@ msgstr "進程啟動"
msgid "Public Key"
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
msgid "Quiet Hours"
msgstr "靜音時段"
#. Disk read
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "讀取"
@@ -1418,15 +1457,15 @@ msgstr "選取{foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "發送單個 heartbeat ping 以驗證您的端點是否正常工作。"
msgstr "發送單次 Heartbeat Ping 以驗證您的 Endpoint 是否運作正常。"
#: 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."
msgstr "定期向外部監控服務發送出站 ping以便您在不將 Beszel 暴露於網際網路的情況下進行監控。"
msgstr "定期發送 Outbound Ping 至外部監控服務,讓您無需將 Beszel 暴露於網際網路即可進行監控。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "發送測試 heartbeat"
msgstr "發送測試 Heartbeat"
#: src/components/routes/system/charts/network-charts.tsx
msgid "Sent"
@@ -1451,7 +1490,7 @@ msgstr "設定儀表顏色的百分比閾值。"
#: src/components/routes/settings/heartbeat.tsx
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
@@ -1549,7 +1588,7 @@ msgstr "列表"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
msgstr "分頁"
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1580,7 +1619,7 @@ msgstr "測試<0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "測試 heartbeat"
msgstr "測試 Heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
@@ -1588,7 +1627,7 @@ msgstr "已發送測試通知"
#: 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."
msgstr "當所有系統都正常運行時,整體狀態為 <0>ok</0>;當觸發警報時為 <1>警告</1>;當任何系統故障時為 <2>錯誤</2>。"
msgstr "當所有系統都正常運行時,整體狀態為 <0>正常</0>;當觸發警報時為 <1>警告</1>;當任何系統故障時為 <2>錯誤</2>。"
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
@@ -1602,7 +1641,7 @@ msgstr "此操作無法復原。這將永久刪除資料庫中{name}的所有當
msgid "This will permanently delete all selected records from the database."
msgstr "這將從資料庫中永久刪除所有選定的記錄。"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Throughput of {extraFsName}"
msgstr "{extraFsName}的傳輸速率"
@@ -1655,6 +1694,11 @@ msgstr "每個介面的總接收資料量"
msgid "Total data sent for each interface"
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
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
@@ -1775,7 +1819,7 @@ msgstr "上傳"
msgid "Uptime"
msgstr "運行時間"
#: src/components/routes/system/charts/extra-fs-charts.tsx
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
#: src/components/routes/system/charts/gpu-charts.tsx
@@ -1797,6 +1841,11 @@ msgstr "已使用"
msgid "Users"
msgstr "使用者"
#: src/components/routes/system/charts/disk-charts.tsx
msgctxt "Disk I/O utilization"
msgid "Utilization"
msgstr "利用率"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "值"
@@ -1806,6 +1855,7 @@ msgid "View"
msgstr "檢視"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/disk-io-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "查看更多"
@@ -1848,7 +1898,7 @@ msgstr "啟用後,此 Token 可讓 Agent 自行註冊,無需預先在系統
#: 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."
msgstr "使用 POST 時,每個 heartbeat 都包含一個 JSON 有效載荷,其中包含系統狀態摘要、故障系統列表和觸發的警報。"
msgstr "使用 POST 時,每個 Heartbeat 都包含一個 JSON Payload內容涵蓋系統狀態概況、離線系統清單以及已觸發的警報。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -1858,7 +1908,9 @@ msgstr "Windows 指令"
#. Disk write
#: src/components/routes/system/charts/disk-charts.tsx
#: src/components/routes/system/charts/extra-fs-charts.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"
msgstr "寫入"

View File

@@ -124,6 +124,10 @@ export interface SystemStats {
dio?: [number, number]
/** max disk I/O bytes [read, write] */
diom?: [number, number]
/** disk io stats [read time factor, write time factor, io utilization %, r_await ms, w_await ms, weighted io %] */
dios?: [number, number, number, number, number, number]
/** max disk io stats */
diosm?: [number, number, number, number, number, number]
/** network sent (mb) */
ns: number
/** network received (mb) */
@@ -186,6 +190,10 @@ export interface ExtraFsStats {
rbm: number
/** max write per second (mb) */
wbm: number
/** disk io stats [read time factor, write time factor, io utilization %, r_await ms, w_await ms, weighted io %] */
dios?: [number, number, number, number, number, number]
/** max disk io stats */
diosm?: [number, number, number, number, number, number]
}
export interface ContainerStatsRecord extends RecordModel {
@@ -413,118 +421,118 @@ export interface SystemdRecord extends RecordModel {
}
export interface SystemdServiceDetails {
AccessSELinuxContext: string;
ActivationDetails: any[];
ActiveEnterTimestamp: number;
ActiveEnterTimestampMonotonic: number;
ActiveExitTimestamp: number;
ActiveExitTimestampMonotonic: number;
ActiveState: string;
After: string[];
AllowIsolate: boolean;
AssertResult: boolean;
AssertTimestamp: number;
AssertTimestampMonotonic: number;
Asserts: any[];
Before: string[];
BindsTo: any[];
BoundBy: any[];
CPUUsageNSec: number;
CanClean: any[];
CanFreeze: boolean;
CanIsolate: boolean;
CanLiveMount: boolean;
CanReload: boolean;
CanStart: boolean;
CanStop: boolean;
CollectMode: string;
ConditionResult: boolean;
ConditionTimestamp: number;
ConditionTimestampMonotonic: number;
Conditions: any[];
ConflictedBy: any[];
Conflicts: string[];
ConsistsOf: any[];
DebugInvocation: boolean;
DefaultDependencies: boolean;
Description: string;
Documentation: string[];
DropInPaths: any[];
ExecMainPID: number;
FailureAction: string;
FailureActionExitStatus: number;
Following: string;
FragmentPath: string;
FreezerState: string;
Id: string;
IgnoreOnIsolate: boolean;
InactiveEnterTimestamp: number;
InactiveEnterTimestampMonotonic: number;
InactiveExitTimestamp: number;
InactiveExitTimestampMonotonic: number;
InvocationID: string;
Job: Array<number | string>;
JobRunningTimeoutUSec: number;
JobTimeoutAction: string;
JobTimeoutRebootArgument: string;
JobTimeoutUSec: number;
JoinsNamespaceOf: any[];
LoadError: string[];
LoadState: string;
MainPID: number;
Markers: any[];
MemoryCurrent: number;
MemoryLimit: number;
MemoryPeak: number;
NRestarts: number;
Names: string[];
NeedDaemonReload: boolean;
OnFailure: any[];
OnFailureJobMode: string;
OnFailureOf: any[];
OnSuccess: any[];
OnSuccessJobMode: string;
OnSuccessOf: any[];
PartOf: any[];
Perpetual: boolean;
PropagatesReloadTo: any[];
PropagatesStopTo: any[];
RebootArgument: string;
Refs: any[];
RefuseManualStart: boolean;
RefuseManualStop: boolean;
ReloadPropagatedFrom: any[];
RequiredBy: any[];
Requires: string[];
RequiresMountsFor: any[];
Requisite: any[];
RequisiteOf: any[];
Result: string;
SliceOf: any[];
SourcePath: string;
StartLimitAction: string;
StartLimitBurst: number;
StartLimitIntervalUSec: number;
StateChangeTimestamp: number;
StateChangeTimestampMonotonic: number;
StopPropagatedFrom: any[];
StopWhenUnneeded: boolean;
SubState: string;
SuccessAction: string;
SuccessActionExitStatus: number;
SurviveFinalKillSignal: boolean;
TasksCurrent: number;
TasksMax: number;
Transient: boolean;
TriggeredBy: string[];
Triggers: any[];
UnitFilePreset: string;
UnitFileState: string;
UpheldBy: any[];
Upholds: any[];
WantedBy: any[];
Wants: string[];
WantsMountsFor: any[];
AccessSELinuxContext: string
ActivationDetails: any[]
ActiveEnterTimestamp: number
ActiveEnterTimestampMonotonic: number
ActiveExitTimestamp: number
ActiveExitTimestampMonotonic: number
ActiveState: string
After: string[]
AllowIsolate: boolean
AssertResult: boolean
AssertTimestamp: number
AssertTimestampMonotonic: number
Asserts: any[]
Before: string[]
BindsTo: any[]
BoundBy: any[]
CPUUsageNSec: number
CanClean: any[]
CanFreeze: boolean
CanIsolate: boolean
CanLiveMount: boolean
CanReload: boolean
CanStart: boolean
CanStop: boolean
CollectMode: string
ConditionResult: boolean
ConditionTimestamp: number
ConditionTimestampMonotonic: number
Conditions: any[]
ConflictedBy: any[]
Conflicts: string[]
ConsistsOf: any[]
DebugInvocation: boolean
DefaultDependencies: boolean
Description: string
Documentation: string[]
DropInPaths: any[]
ExecMainPID: number
FailureAction: string
FailureActionExitStatus: number
Following: string
FragmentPath: string
FreezerState: string
Id: string
IgnoreOnIsolate: boolean
InactiveEnterTimestamp: number
InactiveEnterTimestampMonotonic: number
InactiveExitTimestamp: number
InactiveExitTimestampMonotonic: number
InvocationID: string
Job: Array<number | string>
JobRunningTimeoutUSec: number
JobTimeoutAction: string
JobTimeoutRebootArgument: string
JobTimeoutUSec: number
JoinsNamespaceOf: any[]
LoadError: string[]
LoadState: string
MainPID: number
Markers: any[]
MemoryCurrent: number
MemoryLimit: number
MemoryPeak: number
NRestarts: number
Names: string[]
NeedDaemonReload: boolean
OnFailure: any[]
OnFailureJobMode: string
OnFailureOf: any[]
OnSuccess: any[]
OnSuccessJobMode: string
OnSuccessOf: any[]
PartOf: any[]
Perpetual: boolean
PropagatesReloadTo: any[]
PropagatesStopTo: any[]
RebootArgument: string
Refs: any[]
RefuseManualStart: boolean
RefuseManualStop: boolean
ReloadPropagatedFrom: any[]
RequiredBy: any[]
Requires: string[]
RequiresMountsFor: any[]
Requisite: any[]
RequisiteOf: any[]
Result: string
SliceOf: any[]
SourcePath: string
StartLimitAction: string
StartLimitBurst: number
StartLimitIntervalUSec: number
StateChangeTimestamp: number
StateChangeTimestampMonotonic: number
StopPropagatedFrom: any[]
StopWhenUnneeded: boolean
SubState: string
SuccessAction: string
SuccessActionExitStatus: number
SurviveFinalKillSignal: boolean
TasksCurrent: number
TasksMax: number
Transient: boolean
TriggeredBy: string[]
Triggers: any[]
UnitFilePreset: string
UnitFileState: string
UpheldBy: any[]
Upholds: any[]
WantedBy: any[]
Wants: string[]
WantsMountsFor: any[]
}
export interface BeszelInfo {

View File

@@ -77,6 +77,16 @@ func CreateUser(app core.App, email string, password string) (*core.Record, erro
return user, app.Save(user)
}
// Helper function to create a test superuser for config tests
func CreateSuperuser(app core.App, email string, password string) (*core.Record, error) {
superusersCollection, _ := app.FindCachedCollectionByNameOrId(core.CollectionNameSuperusers)
superuser := core.NewRecord(superusersCollection)
superuser.Set("email", email)
superuser.Set("password", password)
return superuser, app.Save(superuser)
}
func CreateUserWithRole(app core.App, email string, password string, roleName string) (*core.Record, error) {
user, err := CreateUser(app, email, password)
if err != nil {

View File

@@ -1,3 +1,45 @@
## 0.18.7
- Add more disk I/O metrics (utilization, read/write time, await, queue depth) (#1866)
- Add ability to copy alerts between systems (#1853)
- Add `SENSORS_TIMEOUT` environment variable (#1871)
- Replace `distatus/battery` with an internal implementation (#1872)
- Restrict universal token API to non-superuser accounts (#1870)
- Fix macOS ARM64 crashes by upgrading `gopsutil` to v4.26.3 (#1881, #796)
- Fix text size for system names in grid view (#1860)
- Fix NVMe capacity reporting for Apple SSDs (#1873)
- Fix Windows root disk detection when the executable is not on the root disk (#1863)
- Fix nested virtual filesystem inclusion in Docker when mounting host root (#1859)
- Fix OPNsense installation persistence by using the daemon user (#1880)
- Upgrade JS dependencies with dependabot security alerts (#1882)
- Upgrade PocketBase to latest version
## 0.18.6
- Add apple-touch-icon link to index.html (#1850)
- Fix UI bug where charts did not display 1m max until next update
- Fix regression in partition discovery on Docker (#1847)
- Fix agent detection of Podman when using socket proxy (#1846)
- Fix NVML GPU collection being disabled when `nvidia-smi` is not in PATH (#1849)
- Reset SMART interval on agent reconnect if the agent hasn't collected SMART data, allowing config changes to take effect immediately
## 0.18.5
- Add "update available" notification in hub web UI with `CHECK_UPDATES=true` (#1830)

View File

@@ -12,6 +12,10 @@ is_freebsd() {
[ "$(uname -s)" = "FreeBSD" ]
}
is_opnsense() {
[ -f /usr/local/sbin/opnsense-version ] || [ -f /usr/local/etc/opnsense-version ] || [ -f /etc/opnsense-release ]
}
is_glibc() {
# Prefer glibc-enabled agent (NVML via purego) on linux/amd64 glibc systems.
# Check common dynamic loader paths first (fast + reliable).
@@ -549,6 +553,7 @@ else
fi
# Create a dedicated user for the service if it doesn't exist
AGENT_USER="beszel"
echo "Configuring the dedicated user for the Beszel Agent service..."
if is_alpine; then
if ! id -u beszel >/dev/null 2>&1; then
@@ -590,13 +595,18 @@ elif is_openwrt; then
fi
elif is_freebsd; then
if ! id -u beszel >/dev/null 2>&1; then
pw user add beszel -d /nonexistent -s /usr/sbin/nologin -c "beszel user"
fi
# Add the user to the wheel group to allow self-updates
if pw group show wheel >/dev/null 2>&1; then
echo "Adding beszel to wheel group for self-updates"
pw group mod wheel -m beszel
if is_opnsense; then
echo "OPNsense detected: skipping user creation (using daemon user instead)"
AGENT_USER="daemon"
else
if ! id -u beszel >/dev/null 2>&1; then
pw user add beszel -d /nonexistent -s /usr/sbin/nologin -c "beszel user"
fi
# Add the user to the wheel group to allow self-updates
if pw group show wheel >/dev/null 2>&1; then
echo "Adding beszel to wheel group for self-updates"
pw group mod wheel -m beszel
fi
fi
else
@@ -620,7 +630,7 @@ fi
if [ ! -d "$AGENT_DIR" ]; then
echo "Creating the directory for the Beszel Agent..."
mkdir -p "$AGENT_DIR"
chown beszel:beszel "$AGENT_DIR"
chown "${AGENT_USER}:${AGENT_USER}" "$AGENT_DIR"
chmod 755 "$AGENT_DIR"
fi
@@ -899,7 +909,7 @@ TOKEN=$TOKEN
HUB_URL=$HUB_URL
EOF
chmod 640 "$AGENT_DIR/env"
chown root:beszel "$AGENT_DIR/env"
chown "root:${AGENT_USER}" "$AGENT_DIR/env"
else
echo "FreeBSD environment file already exists. Skipping creation."
fi
@@ -917,6 +927,7 @@ EOF
# Enable and start the service
echo "Enabling and starting the agent service..."
sysrc beszel_agent_enable="YES"
sysrc beszel_agent_user="${AGENT_USER}"
service beszel-agent restart
# Check if service started successfully