Compare commits

..

31 Commits

Author SHA1 Message Date
henrygd
5eca353429 updates 2026-03-07 12:19:18 -05:00
henrygd
d9e3c4678a updates 2026-03-05 16:10:19 -05:00
henrygd
1243a7bd8d update 2026-03-05 14:29:02 -05:00
henrygd
bd74ab8d7b updates 2026-03-03 19:08:28 -05:00
henrygd
016d775675 more updates 2026-03-03 16:32:50 -05:00
henrygd
bdbd135fdd updates 2026-03-03 14:23:42 -05:00
henrygd
48503f9f99 updates 2026-03-03 12:42:30 -05:00
henrygd
d34ef1ebe9 updates 2026-03-02 14:10:26 -05:00
henrygd
8f23fff1c9 refactor: mdraid comments and organization
also hide serial / firmware in smart details if empty, remove a few
unnecessary ops, and add a few more passed state values
2026-02-27 14:23:10 -05:00
VACInc
02c1a0c13d Add Linux mdraid health monitoring (#1750) 2026-02-27 13:42:47 -05:00
henrygd
69fdcb36ab support ZFS ARC on freebsd 2026-02-26 18:38:54 -05:00
henrygd
b91eb6de40 improve root I/O device detection and fallback (#1772)
- Match FILESYSTEM directly against I/O devices if partition lookup
fails
- Fall back to the most active I/O device if no root device is detected
- Add WARN logs in final fallback case to most active device
2026-02-26 18:11:33 -05:00
henrygd
ec69f6c6e0 improve disk I/O device matching for partition-to-disk mismatches (#1772)
findIoDevice now normalizes device names and falls back to prefix-based
matching when partition names differ from IOCounter names (e.g. nda0p2 →
nda0 on FreeBSD). The most-active prefix-related device is selected,
avoiding the broad "most active of all" heuristic that caused Docker
misattribution in #1737.
2026-02-26 16:59:12 -05:00
henrygd
a86cb91e07 improve install scripts with retries, validation, and better error messages
Add curl retries/timeouts, archive integrity checks, binary existence
checks, and temp dir cleanup on all failure paths. Unify --mirror flag
handling in hub script to match agent. Use cat instead of tee for
systemd service file, quiet systemctl output.
2026-02-26 12:29:05 -05:00
henrygd
004841717a add checks for non-empty CPU times during initialization (#401) 2026-02-25 19:04:29 -05:00
henrygd
096296ba7b fix: ensure rc.d directory exists for minimal FreeBSD installs in install-agent.sh 2026-02-25 16:22:37 -05:00
ilya
b012df5669 Fix volume path in Docker run command (#1764) 2026-02-24 15:47:16 -05:00
henrygd
12545b4b6d fix: dedupe root-mirrored extra filesystems during disk discovery (#1428) 2026-02-24 15:41:29 -05:00
henrygd
9e2296452b fix: compute bandwidth alerts from byte-per-second source (#1770)
Use Info.BandwidthBytes converted to MB/s with float division so
bandwidth alert checks are based on current data without integer
truncation near thresholds.
2026-02-24 13:07:27 -05:00
henrygd
ac79860d4a dev: update biome schema and disable assist/source/organizeImports 2026-02-20 15:50:44 -05:00
henrygd
e13a99fdac ui: add fallback to display language code if no emoji / flag 2026-02-20 15:46:24 -05:00
henrygd
4cfb2a86ad 0.18.4 release 2026-02-20 15:00:15 -05:00
henrygd
191f25f6e0 ui: refactor heartbeat settings page 2026-02-20 14:48:59 -05:00
henrygd
aa8b3711d7 update translations 2026-02-19 19:22:54 -05:00
henrygd
1fb0b25988 testing: improve flaky hub cleanup in agent_connect_test.go 2026-02-19 18:35:31 -05:00
henrygd
04600d83cc refactor: small go 1.26 updates and go fix changes 2026-02-19 18:04:33 -05:00
henrygd
5d8906c9b2 amd gpu: small refactor + trim "series" from device name 2026-02-19 17:39:13 -05:00
henrygd
daac287b9d ui: fix race issue with meter threshold colors
also increase the default container width
2026-02-19 17:37:57 -05:00
henrygd
d526ea61a9 ui: freeze header of smart device details table 2026-02-19 17:35:12 -05:00
henrygd
79616e1662 update translations 2026-02-19 16:21:59 -05:00
Sven van Ginkel
01e8bdf040 feat: allow precise value entry for alerts via text input (#1718) 2026-02-19 13:15:12 -05:00
121 changed files with 5639 additions and 638 deletions

View File

@@ -33,6 +33,7 @@ type Agent struct {
netIoStats map[uint16]system.NetIoStats // Keeps track of bandwidth usage per cache interval
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
dockerManager *dockerManager // Manages Docker API requests
pveManager *pveManager // Manages Proxmox VE API requests
sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info (dynamic)
systemDetails system.Details // Host system details (static, once-per-connection)
@@ -99,6 +100,9 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// initialize docker manager
agent.dockerManager = newDockerManager()
// initialize pve manager
agent.pveManager = newPVEManager()
// initialize system info
agent.refreshSystemDetails()
@@ -189,6 +193,15 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD
}
}
if a.pveManager != nil {
if pveStats, err := a.pveManager.getPVEStats(); err == nil {
data.PVEStats = pveStats
slog.Debug("PVE", "data", data.PVEStats)
} else {
slog.Debug("PVE", "err", err)
}
}
// skip updating systemd services if cache time is not the default 60sec interval
if a.systemdManager != nil && cacheTimeMs == 60_000 {
totalCount := uint16(a.systemdManager.getServiceStatsCount())

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent
@@ -15,6 +14,10 @@ import (
)
func createTestCacheData() *system.CombinedData {
var stats = container.Stats{}
stats.Name = "test-container"
stats.Cpu = 10.5
stats.Mem = 1073741824 // 1GB
return &system.CombinedData{
Stats: system.Stats{
Cpu: 50.5,
@@ -25,10 +28,7 @@ func createTestCacheData() *system.CombinedData {
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{
{
Name: "test-container",
Cpu: 25.0,
},
&stats,
},
}
}

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -1,9 +1,9 @@
package agent
import (
"context"
"errors"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
@@ -91,8 +91,8 @@ func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
c.eventChan = make(chan ConnectionEvent, 1)
// signal handling for shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
sigCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stopSignals()
c.startWsTicker()
c.connect()
@@ -109,8 +109,8 @@ func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
_ = c.startWebSocketConnection()
case <-healthTicker:
_ = health.Update()
case <-sigChan:
slog.Info("Shutting down")
case <-sigCtx.Done():
slog.Info("Shutting down", "cause", context.Cause(sigCtx))
_ = c.agent.StopServer()
c.closeWebSocket()
return health.CleanUp()

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -14,10 +14,10 @@ var lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)
// init initializes the CPU monitoring by storing the initial CPU times
// for the default 60-second cache interval.
func init() {
if times, err := cpu.Times(false); err == nil {
if times, err := cpu.Times(false); err == nil && len(times) > 0 {
lastCpuTimes[60000] = times[0]
}
if perCoreTimes, err := cpu.Times(true); err == nil {
if perCoreTimes, err := cpu.Times(true); err == nil && len(perCoreTimes) > 0 {
lastPerCoreCpuTimes[60000] = perCoreTimes
}
}
@@ -89,10 +89,7 @@ func getPerCoreCpuUsage(cacheTimeMs uint16) (system.Uint8Slice, error) {
lastTimes := lastPerCoreCpuTimes[cacheTimeMs]
// Limit to the number of cores available in both samples
length := len(perCoreTimes)
if len(lastTimes) < length {
length = len(lastTimes)
}
length := min(len(lastTimes), len(perCoreTimes))
usage := make([]uint8, length)
for i := 0; i < length; i++ {

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -78,14 +78,21 @@ func (a *Agent) initializeDiskInfo() {
if _, exists := a.fsStats[key]; !exists {
if root {
slog.Info("Detected root device", "name", key)
// Check if root device is in /proc/diskstats. Do not guess a
// fallback device for root: that can misattribute root I/O to a
// different disk while usage remains tied to root mountpoint.
// Try to map root device to a diskIoCounters entry. First
// checks for an exact key match, then uses findIoDevice for
// normalized / prefix-based matching (e.g. nda0p2 → nda0),
// and finally falls back to the FILESYSTEM env var.
if _, ioMatch = diskIoCounters[key]; !ioMatch {
if matchedKey, match := findIoDevice(filesystem, diskIoCounters); match {
if matchedKey, match := findIoDevice(key, diskIoCounters); match {
key = matchedKey
ioMatch = true
} else {
} else if filesystem != "" {
if matchedKey, match := findIoDevice(filesystem, diskIoCounters); match {
key = matchedKey
ioMatch = true
}
}
if !ioMatch {
slog.Warn("Root I/O unmapped; set FILESYSTEM", "device", device, "mountpoint", mountpoint)
}
}
@@ -114,20 +121,28 @@ func (a *Agent) initializeDiskInfo() {
// Use FILESYSTEM env var to find root filesystem
if filesystem != "" {
for _, p := range partitions {
if strings.HasSuffix(p.Device, filesystem) || p.Mountpoint == filesystem {
if filesystemMatchesPartitionSetting(filesystem, p) {
addFsStat(p.Device, p.Mountpoint, true)
hasRoot = true
break
}
}
if !hasRoot {
slog.Warn("Partition details not found", "filesystem", filesystem)
// FILESYSTEM may name a physical disk absent from partitions (e.g.
// ZFS lists dataset paths like zroot/ROOT/default, not block devices).
// Try matching directly against diskIoCounters.
if ioKey, match := findIoDevice(filesystem, diskIoCounters); match {
a.fsStats[ioKey] = &system.FsStats{Root: true, Mountpoint: rootMountPoint}
hasRoot = true
} else {
slog.Warn("Partition details not found", "filesystem", filesystem)
}
}
}
// Add EXTRA_FILESYSTEMS env var values to fsStats
if extraFilesystems, exists := GetEnv("EXTRA_FILESYSTEMS"); exists {
for _, fsEntry := range strings.Split(extraFilesystems, ",") {
for fsEntry := range strings.SplitSeq(extraFilesystems, ",") {
// Parse custom name from format: device__customname
fs, customName := parseFilesystemEntry(fsEntry)
@@ -187,28 +202,180 @@ func (a *Agent) initializeDiskInfo() {
}
}
// If no root filesystem set, use fallback
// If no root filesystem set, try the most active I/O device as a last
// resort (e.g. ZFS where dataset names are unrelated to disk names).
if !hasRoot {
rootKey := filepath.Base(rootMountPoint)
if _, exists := a.fsStats[rootKey]; exists {
rootKey = "root"
rootKey := mostActiveIoDevice(diskIoCounters)
if rootKey != "" {
slog.Warn("Using most active device for root I/O; set FILESYSTEM to override", "device", rootKey)
} else {
rootKey = filepath.Base(rootMountPoint)
if _, exists := a.fsStats[rootKey]; exists {
rootKey = "root"
}
slog.Warn("Root I/O device not detected; set FILESYSTEM to override")
}
slog.Warn("Root device not detected; root I/O disabled", "mountpoint", rootMountPoint)
a.fsStats[rootKey] = &system.FsStats{Root: true, Mountpoint: rootMountPoint}
}
a.pruneDuplicateRootExtraFilesystems()
a.initializeDiskIoStats(diskIoCounters)
}
// Returns matching device from /proc/diskstats.
// bool is true if a match was found.
func findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCountersStat) (string, bool) {
for _, d := range diskIoCounters {
if d.Name == filesystem || (d.Label != "" && d.Label == filesystem) {
return d.Name, true
// Removes extra filesystems that mirror root usage (https://github.com/henrygd/beszel/issues/1428).
func (a *Agent) pruneDuplicateRootExtraFilesystems() {
var rootMountpoint string
for _, stats := range a.fsStats {
if stats != nil && stats.Root {
rootMountpoint = stats.Mountpoint
break
}
}
return "", false
if rootMountpoint == "" {
return
}
rootUsage, err := disk.Usage(rootMountpoint)
if err != nil {
return
}
for name, stats := range a.fsStats {
if stats == nil || stats.Root {
continue
}
extraUsage, err := disk.Usage(stats.Mountpoint)
if err != nil {
continue
}
if hasSameDiskUsage(rootUsage, extraUsage) {
slog.Info("Ignoring duplicate FS", "name", name, "mount", stats.Mountpoint)
delete(a.fsStats, name)
}
}
}
// hasSameDiskUsage compares root/extra usage with a small byte tolerance.
func hasSameDiskUsage(a, b *disk.UsageStat) bool {
if a == nil || b == nil || a.Total == 0 || b.Total == 0 {
return false
}
// Allow minor drift between sequential disk usage calls.
const toleranceBytes uint64 = 16 * 1024 * 1024
return withinUsageTolerance(a.Total, b.Total, toleranceBytes) &&
withinUsageTolerance(a.Used, b.Used, toleranceBytes)
}
// withinUsageTolerance reports whether two byte values differ by at most tolerance.
func withinUsageTolerance(a, b, tolerance uint64) bool {
if a >= b {
return a-b <= tolerance
}
return b-a <= tolerance
}
type ioMatchCandidate struct {
name string
bytes uint64
ops uint64
}
// findIoDevice prefers exact device/label matches, then falls back to a
// prefix-related candidate with the highest recent activity.
func findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCountersStat) (string, bool) {
filesystem = normalizeDeviceName(filesystem)
if filesystem == "" {
return "", false
}
candidates := []ioMatchCandidate{}
for _, d := range diskIoCounters {
if normalizeDeviceName(d.Name) == filesystem || (d.Label != "" && normalizeDeviceName(d.Label) == filesystem) {
return d.Name, true
}
if prefixRelated(normalizeDeviceName(d.Name), filesystem) ||
(d.Label != "" && prefixRelated(normalizeDeviceName(d.Label), filesystem)) {
candidates = append(candidates, ioMatchCandidate{
name: d.Name,
bytes: d.ReadBytes + d.WriteBytes,
ops: d.ReadCount + d.WriteCount,
})
}
}
if len(candidates) == 0 {
return "", false
}
best := candidates[0]
for _, c := range candidates[1:] {
if c.bytes > best.bytes ||
(c.bytes == best.bytes && c.ops > best.ops) ||
(c.bytes == best.bytes && c.ops == best.ops && c.name < best.name) {
best = c
}
}
slog.Info("Using disk I/O fallback", "requested", filesystem, "selected", best.name)
return best.name, true
}
// mostActiveIoDevice returns the device with the highest I/O activity,
// or "" if diskIoCounters is empty.
func mostActiveIoDevice(diskIoCounters map[string]disk.IOCountersStat) string {
var best ioMatchCandidate
for _, d := range diskIoCounters {
c := ioMatchCandidate{
name: d.Name,
bytes: d.ReadBytes + d.WriteBytes,
ops: d.ReadCount + d.WriteCount,
}
if best.name == "" || c.bytes > best.bytes ||
(c.bytes == best.bytes && c.ops > best.ops) ||
(c.bytes == best.bytes && c.ops == best.ops && c.name < best.name) {
best = c
}
}
return best.name
}
// prefixRelated reports whether either identifier is a prefix of the other.
func prefixRelated(a, b string) bool {
if a == "" || b == "" || a == b {
return false
}
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
}
// filesystemMatchesPartitionSetting checks whether a FILESYSTEM env var value
// matches a partition by mountpoint, exact device name, or prefix relationship
// (e.g. FILESYSTEM=ada0 matches partition /dev/ada0p2).
func filesystemMatchesPartitionSetting(filesystem string, p disk.PartitionStat) bool {
filesystem = strings.TrimSpace(filesystem)
if filesystem == "" {
return false
}
if p.Mountpoint == filesystem {
return true
}
fsName := normalizeDeviceName(filesystem)
partName := normalizeDeviceName(p.Device)
if fsName == "" || partName == "" {
return false
}
if fsName == partName {
return true
}
return prefixRelated(partName, fsName)
}
// normalizeDeviceName canonicalizes device strings for comparisons.
func normalizeDeviceName(value string) string {
name := filepath.Base(strings.TrimSpace(value))
if name == "." {
return ""
}
return name
}
// Sets start values for disk I/O stats.

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent
@@ -117,7 +116,7 @@ func TestFindIoDevice(t *testing.T) {
assert.Equal(t, "sda", device)
})
t.Run("returns no fallback when not found", func(t *testing.T) {
t.Run("returns no match when not found", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda"},
"sdb": {Name: "sdb"},
@@ -127,6 +126,106 @@ func TestFindIoDevice(t *testing.T) {
assert.False(t, ok)
assert.Equal(t, "", device)
})
t.Run("uses uncertain unique prefix fallback", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"nvme0n1": {Name: "nvme0n1"},
"sda": {Name: "sda"},
}
device, ok := findIoDevice("nvme0n1p2", ioCounters)
assert.True(t, ok)
assert.Equal(t, "nvme0n1", device)
})
t.Run("uses dominant activity when prefix matches are ambiguous", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},
}
device, ok := findIoDevice("sd", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
t.Run("uses highest activity when ambiguous without dominance", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda", ReadBytes: 3000, WriteBytes: 3000, ReadCount: 50, WriteCount: 50},
"sdb": {Name: "sdb", ReadBytes: 2500, WriteBytes: 2500, ReadCount: 40, WriteCount: 40},
}
device, ok := findIoDevice("sd", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
t.Run("matches /dev/-prefixed partition to parent disk", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"nda0": {Name: "nda0", ReadBytes: 1000, WriteBytes: 1000},
}
device, ok := findIoDevice("/dev/nda0p2", ioCounters)
assert.True(t, ok)
assert.Equal(t, "nda0", device)
})
t.Run("uses deterministic name tie-breaker", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sdb": {Name: "sdb", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},
"sda": {Name: "sda", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},
}
device, ok := findIoDevice("sd", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
}
func TestFilesystemMatchesPartitionSetting(t *testing.T) {
p := disk.PartitionStat{Device: "/dev/ada0p2", Mountpoint: "/"}
t.Run("matches mountpoint setting", func(t *testing.T) {
assert.True(t, filesystemMatchesPartitionSetting("/", p))
})
t.Run("matches exact partition setting", func(t *testing.T) {
assert.True(t, filesystemMatchesPartitionSetting("ada0p2", p))
assert.True(t, filesystemMatchesPartitionSetting("/dev/ada0p2", p))
})
t.Run("matches prefix-style parent setting", func(t *testing.T) {
assert.True(t, filesystemMatchesPartitionSetting("ada0", p))
assert.True(t, filesystemMatchesPartitionSetting("/dev/ada0", p))
})
t.Run("does not match unrelated device", func(t *testing.T) {
assert.False(t, filesystemMatchesPartitionSetting("sda", p))
assert.False(t, filesystemMatchesPartitionSetting("nvme0n1", p))
assert.False(t, filesystemMatchesPartitionSetting("", p))
})
}
func TestMostActiveIoDevice(t *testing.T) {
t.Run("returns most active device", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"nda0": {Name: "nda0", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},
"nda1": {Name: "nda1", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},
}
assert.Equal(t, "nda0", mostActiveIoDevice(ioCounters))
})
t.Run("uses deterministic tie-breaker", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},
"sda": {Name: "sda", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},
}
assert.Equal(t, "sda", mostActiveIoDevice(ioCounters))
})
t.Run("returns empty for empty map", func(t *testing.T) {
assert.Equal(t, "", mostActiveIoDevice(map[string]disk.IOCountersStat{}))
})
}
func TestIsDockerSpecialMountpoint(t *testing.T) {
@@ -373,3 +472,37 @@ func TestDiskUsageCaching(t *testing.T) {
"lastDiskUsageUpdate should be refreshed when cache expires")
})
}
func TestHasSameDiskUsage(t *testing.T) {
const toleranceBytes uint64 = 16 * 1024 * 1024
t.Run("returns true when totals and usage are equal", func(t *testing.T) {
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
b := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
assert.True(t, hasSameDiskUsage(a, b))
})
t.Run("returns true within tolerance", func(t *testing.T) {
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
b := &disk.UsageStat{
Total: a.Total + toleranceBytes - 1,
Used: a.Used - toleranceBytes + 1,
}
assert.True(t, hasSameDiskUsage(a, b))
})
t.Run("returns false when total exceeds tolerance", func(t *testing.T) {
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
b := &disk.UsageStat{
Total: a.Total + toleranceBytes + 1,
Used: a.Used,
}
assert.False(t, hasSameDiskUsage(a, b))
})
t.Run("returns false for nil or zero total", func(t *testing.T) {
assert.False(t, hasSameDiskUsage(nil, &disk.UsageStat{Total: 1, Used: 1}))
assert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 1, Used: 1}, nil))
assert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 0, Used: 0}, &disk.UsageStat{Total: 1, Used: 1}))
})
}

View File

@@ -28,8 +28,10 @@ import (
// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.)
// This includes CSI sequences like \x1b[...m and simple escapes like \x1b[K
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`)
var dockerContainerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
var (
ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`)
dockerContainerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
)
const (
// Docker API timeout in milliseconds
@@ -395,11 +397,12 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeM
// add empty values if they doesn't exist in map
stats, initialized := dm.containerStatsMap[ctr.IdShort]
if !initialized {
stats = &container.Stats{Name: name, Id: ctr.IdShort, Image: ctr.Image}
stats = &container.Stats{Image: ctr.Image}
dm.containerStatsMap[ctr.IdShort] = stats
}
stats.Id = ctr.IdShort
stats.Name = name
statusText, health := parseDockerStatus(ctr.Status)
stats.Status = statusText

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent
@@ -270,17 +269,16 @@ func TestValidateCpuPercentage(t *testing.T) {
}
func TestUpdateContainerStatsValues(t *testing.T) {
stats := &container.Stats{
Name: "test-container",
Cpu: 0.0,
Mem: 0.0,
NetworkSent: 0.0,
NetworkRecv: 0.0,
PrevReadTime: time.Time{},
}
var stats = container.Stats{}
stats.Name = "test-container"
stats.Cpu = 0.0
stats.Mem = 0.0
stats.NetworkSent = 0.0
stats.NetworkRecv = 0.0
stats.PrevReadTime = time.Time{}
testTime := time.Now()
updateContainerStatsValues(stats, 75.5, 1048576, 524288, 262144, testTime)
updateContainerStatsValues(&stats, 75.5, 1048576, 524288, 262144, testTime)
// Check CPU percentage (should be rounded to 2 decimals)
assert.Equal(t, 75.5, stats.Cpu)
@@ -447,12 +445,11 @@ func TestCalculateNetworkStats(t *testing.T) {
},
}
stats := &container.Stats{
PrevReadTime: time.Now().Add(-time.Second), // 1 second ago
}
var stats = container.Stats{}
stats.PrevReadTime = time.Now().Add(-time.Second) // 1 second ago
// Test with initialized container
sent, recv := dm.calculateNetworkStats(ctr, apiStats, stats, true, "test-container", cacheTimeMs)
sent, recv := dm.calculateNetworkStats(ctr, apiStats, &stats, true, "test-container", cacheTimeMs)
// Should return calculated byte rates per second
assert.GreaterOrEqual(t, sent, uint64(0))
@@ -461,7 +458,7 @@ func TestCalculateNetworkStats(t *testing.T) {
// Cycle and test one-direction change (Tx only) is reflected independently
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
apiStats.Networks["eth0"] = container.NetworkStats{TxBytes: 2500, RxBytes: 1800} // +500 Tx only
sent, recv = dm.calculateNetworkStats(ctr, apiStats, stats, true, "test-container", cacheTimeMs)
sent, recv = dm.calculateNetworkStats(ctr, apiStats, &stats, true, "test-container", cacheTimeMs)
assert.Greater(t, sent, uint64(0))
assert.Equal(t, uint64(0), recv)
}
@@ -727,7 +724,8 @@ func TestMemoryStatsEdgeCases(t *testing.T) {
}
func TestContainerStatsInitialization(t *testing.T) {
stats := &container.Stats{Name: "test-container"}
var stats = container.Stats{}
stats.Name = "test-container"
// Verify initial values
assert.Equal(t, "test-container", stats.Name)
@@ -739,7 +737,7 @@ func TestContainerStatsInitialization(t *testing.T) {
// Test updating values
testTime := time.Now()
updateContainerStatsValues(stats, 45.67, 2097152, 1048576, 524288, testTime)
updateContainerStatsValues(&stats, 45.67, 2097152, 1048576, 524288, testTime)
assert.Equal(t, 45.67, stats.Cpu)
assert.Equal(t, 2.0, stats.Mem)
@@ -817,12 +815,11 @@ func TestNetworkStatsCalculationWithRealData(t *testing.T) {
// Use exact timing for deterministic results
exactly1000msAgo := time.Now().Add(-1000 * time.Millisecond)
stats := &container.Stats{
PrevReadTime: exactly1000msAgo,
}
var stats = container.Stats{}
stats.PrevReadTime = exactly1000msAgo
// First call sets baseline
sent1, recv1 := dm.calculateNetworkStats(ctr, apiStats1, stats, true, "test", cacheTimeMs)
sent1, recv1 := dm.calculateNetworkStats(ctr, apiStats1, &stats, true, "test", cacheTimeMs)
assert.Equal(t, uint64(0), sent1)
assert.Equal(t, uint64(0), recv1)
@@ -837,7 +834,7 @@ func TestNetworkStatsCalculationWithRealData(t *testing.T) {
expectedRecvRate := deltaRecv * 1000 / expectedElapsedMs // Should be exactly 1000000
// Second call with changed data
sent2, recv2 := dm.calculateNetworkStats(ctr, apiStats2, stats, true, "test", cacheTimeMs)
sent2, recv2 := dm.calculateNetworkStats(ctr, apiStats2, &stats, true, "test", cacheTimeMs)
// Should be exactly the expected rates (no tolerance needed)
assert.Equal(t, expectedSentRate, sent2)
@@ -848,9 +845,9 @@ func TestNetworkStatsCalculationWithRealData(t *testing.T) {
stats.PrevReadTime = time.Now().Add(-1 * time.Millisecond)
apiStats1.Networks["eth0"] = container.NetworkStats{TxBytes: 0, RxBytes: 0}
apiStats2.Networks["eth0"] = container.NetworkStats{TxBytes: 10 * 1024 * 1024 * 1024, RxBytes: 0} // 10GB delta
_, _ = dm.calculateNetworkStats(ctr, apiStats1, stats, true, "test", cacheTimeMs) // baseline
_, _ = dm.calculateNetworkStats(ctr, apiStats1, &stats, true, "test", cacheTimeMs) // baseline
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
sent3, recv3 := dm.calculateNetworkStats(ctr, apiStats2, stats, true, "test", cacheTimeMs)
sent3, recv3 := dm.calculateNetworkStats(ctr, apiStats2, &stats, true, "test", cacheTimeMs)
assert.Equal(t, uint64(0), sent3)
assert.Equal(t, uint64(0), recv3)
}
@@ -884,8 +881,9 @@ func TestContainerStatsEndToEndWithRealData(t *testing.T) {
}
// Initialize container stats
stats := &container.Stats{Name: "jellyfin"}
dm.containerStatsMap[ctr.IdShort] = stats
var stats = container.Stats{}
stats.Name = "jellyfin"
dm.containerStatsMap[ctr.IdShort] = &stats
// Test individual components that we can verify
usedMemory, memErr := calculateMemoryUsage(&apiStats, false)

View File

@@ -199,19 +199,6 @@ func readHexByteFile(path string) (uint8, bool) {
return b, ok
}
func readStringFile(path string) string {
content, _ := readStringFileOK(path)
return content
}
func readStringFileOK(path string) (string, bool) {
b, err := os.ReadFile(path)
if err != nil {
return "", false
}
return strings.TrimSpace(string(b)), true
}
func hasEmmcHealthFiles(deviceDir string) bool {
entries, err := os.ReadDir(deviceDir)
if err != nil {

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

41
agent/fs_utils.go Normal file
View File

@@ -0,0 +1,41 @@
package agent
import (
"os"
"strconv"
"strings"
)
// readStringFile returns trimmed file contents or empty string on error.
func readStringFile(path string) string {
content, _ := readStringFileOK(path)
return content
}
// readStringFileOK returns trimmed file contents and read success.
func readStringFileOK(path string) (string, bool) {
b, err := os.ReadFile(path)
if err != nil {
return "", false
}
return strings.TrimSpace(string(b)), true
}
// fileExists reports whether the given path exists.
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// readUintFile parses a decimal uint64 value from a file.
func readUintFile(path string) (uint64, bool) {
raw, ok := readStringFileOK(path)
if !ok {
return 0, false
}
parsed, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return 0, false
}
return parsed, true
}

View File

@@ -103,10 +103,8 @@ func (gm *GPUManager) updateAmdGpuData(cardPath string) bool {
// Read all sysfs values first (no lock needed - these can be slow)
usage, usageErr := readSysfsFloat(filepath.Join(devicePath, "gpu_busy_percent"))
vramUsed, memUsedErr := readSysfsFloat(filepath.Join(devicePath, "mem_info_vram_used"))
vramTotal, _ := readSysfsFloat(filepath.Join(devicePath, "mem_info_vram_total"))
memUsed := vramUsed
memTotal := vramTotal
memUsed, memUsedErr := readSysfsFloat(filepath.Join(devicePath, "mem_info_vram_used"))
memTotal, _ := readSysfsFloat(filepath.Join(devicePath, "mem_info_vram_total"))
// if gtt is present, add it to the memory used and total (https://github.com/henrygd/beszel/issues/1569#issuecomment-3837640484)
if gttUsed, err := readSysfsFloat(filepath.Join(devicePath, "mem_info_gtt_used")); err == nil && gttUsed > 0 {
if gttTotal, err := readSysfsFloat(filepath.Join(devicePath, "mem_info_gtt_total")); err == nil {
@@ -243,7 +241,10 @@ func getCachedAmdgpuName(deviceID, revisionID string) (name string, found bool,
// normalizeAmdgpuName trims standard suffixes from AMDGPU product names.
func normalizeAmdgpuName(name string) string {
return strings.TrimSuffix(strings.TrimSpace(name), " Graphics")
for _, suffix := range []string{" Graphics", " Series"} {
name = strings.TrimSuffix(name, suffix)
}
return name
}
// cacheAmdgpuName stores a resolved AMDGPU name in the lookup cache.

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package health

225
agent/mdraid_linux.go Normal file
View File

@@ -0,0 +1,225 @@
//go:build linux
package agent
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/henrygd/beszel/internal/entities/smart"
)
// mdraidSysfsRoot is a test hook; production value is "/sys".
var mdraidSysfsRoot = "/sys"
type mdraidHealth struct {
level string
arrayState string
degraded uint64
raidDisks uint64
syncAction string
syncCompleted string
syncSpeed string
mismatchCnt uint64
capacity uint64
}
// scanMdraidDevices discovers Linux md arrays exposed in sysfs.
func scanMdraidDevices() []*DeviceInfo {
blockDir := filepath.Join(mdraidSysfsRoot, "block")
entries, err := os.ReadDir(blockDir)
if err != nil {
return nil
}
devices := make([]*DeviceInfo, 0, 2)
for _, ent := range entries {
name := ent.Name()
if !isMdraidBlockName(name) {
continue
}
mdDir := filepath.Join(blockDir, name, "md")
if !fileExists(filepath.Join(mdDir, "array_state")) {
continue
}
devPath := filepath.Join("/dev", name)
devices = append(devices, &DeviceInfo{
Name: devPath,
Type: "mdraid",
InfoName: devPath + " [mdraid]",
Protocol: "MD",
})
}
return devices
}
// collectMdraidHealth reads mdraid health and stores it in SmartDataMap.
func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error) {
if deviceInfo == nil || deviceInfo.Name == "" {
return false, nil
}
base := filepath.Base(deviceInfo.Name)
if !isMdraidBlockName(base) && !strings.EqualFold(deviceInfo.Type, "mdraid") {
return false, nil
}
health, ok := readMdraidHealth(base)
if !ok {
return false, nil
}
deviceInfo.Type = "mdraid"
key := fmt.Sprintf("mdraid:%s", base)
status := mdraidSmartStatus(health)
attrs := make([]*smart.SmartAttribute, 0, 10)
if health.arrayState != "" {
attrs = append(attrs, &smart.SmartAttribute{Name: "ArrayState", RawString: health.arrayState})
}
if health.level != "" {
attrs = append(attrs, &smart.SmartAttribute{Name: "RaidLevel", RawString: health.level})
}
if health.raidDisks > 0 {
attrs = append(attrs, &smart.SmartAttribute{Name: "RaidDisks", RawValue: health.raidDisks})
}
if health.degraded > 0 {
attrs = append(attrs, &smart.SmartAttribute{Name: "Degraded", RawValue: health.degraded})
}
if health.syncAction != "" {
attrs = append(attrs, &smart.SmartAttribute{Name: "SyncAction", RawString: health.syncAction})
}
if health.syncCompleted != "" {
attrs = append(attrs, &smart.SmartAttribute{Name: "SyncCompleted", RawString: health.syncCompleted})
}
if health.syncSpeed != "" {
attrs = append(attrs, &smart.SmartAttribute{Name: "SyncSpeed", RawString: health.syncSpeed})
}
if health.mismatchCnt > 0 {
attrs = append(attrs, &smart.SmartAttribute{Name: "MismatchCount", RawValue: health.mismatchCnt})
}
sm.Lock()
defer sm.Unlock()
if _, exists := sm.SmartDataMap[key]; !exists {
sm.SmartDataMap[key] = &smart.SmartData{}
}
data := sm.SmartDataMap[key]
data.ModelName = "Linux MD RAID"
if health.level != "" {
data.ModelName = "Linux MD RAID (" + health.level + ")"
}
data.Capacity = health.capacity
data.SmartStatus = status
data.DiskName = filepath.Join("/dev", base)
data.DiskType = "mdraid"
data.Attributes = attrs
return true, nil
}
// readMdraidHealth reads md array health fields from sysfs.
func readMdraidHealth(blockName string) (mdraidHealth, bool) {
var out mdraidHealth
if !isMdraidBlockName(blockName) {
return out, false
}
mdDir := filepath.Join(mdraidSysfsRoot, "block", blockName, "md")
arrayState, okState := readStringFileOK(filepath.Join(mdDir, "array_state"))
if !okState {
return out, false
}
out.arrayState = arrayState
out.level = readStringFile(filepath.Join(mdDir, "level"))
out.syncAction = readStringFile(filepath.Join(mdDir, "sync_action"))
out.syncCompleted = readStringFile(filepath.Join(mdDir, "sync_completed"))
out.syncSpeed = readStringFile(filepath.Join(mdDir, "sync_speed"))
if val, ok := readUintFile(filepath.Join(mdDir, "raid_disks")); ok {
out.raidDisks = val
}
if val, ok := readUintFile(filepath.Join(mdDir, "degraded")); ok {
out.degraded = val
}
if val, ok := readUintFile(filepath.Join(mdDir, "mismatch_cnt")); ok {
out.mismatchCnt = val
}
if capBytes, ok := readMdraidBlockCapacityBytes(blockName, mdraidSysfsRoot); ok {
out.capacity = capBytes
}
return out, true
}
// mdraidSmartStatus maps md state/sync signals to a SMART-like status.
func mdraidSmartStatus(health mdraidHealth) string {
state := strings.ToLower(strings.TrimSpace(health.arrayState))
switch state {
case "inactive", "faulty", "broken", "stopped":
return "FAILED"
}
if health.degraded > 0 {
return "FAILED"
}
switch strings.ToLower(strings.TrimSpace(health.syncAction)) {
case "resync", "recover", "reshape", "check", "repair":
return "WARNING"
}
switch state {
case "clean", "active", "active-idle", "write-pending", "read-auto", "readonly":
return "PASSED"
}
return "UNKNOWN"
}
// isMdraidBlockName matches /dev/mdN-style block device names.
func isMdraidBlockName(name string) bool {
if !strings.HasPrefix(name, "md") {
return false
}
suffix := strings.TrimPrefix(name, "md")
if suffix == "" {
return false
}
for _, c := range suffix {
if c < '0' || c > '9' {
return false
}
}
return true
}
// readMdraidBlockCapacityBytes converts block size metadata into bytes.
func readMdraidBlockCapacityBytes(blockName, root string) (uint64, bool) {
sizePath := filepath.Join(root, "block", blockName, "size")
lbsPath := filepath.Join(root, "block", blockName, "queue", "logical_block_size")
sizeStr, ok := readStringFileOK(sizePath)
if !ok {
return 0, false
}
sectors, err := strconv.ParseUint(sizeStr, 10, 64)
if err != nil || sectors == 0 {
return 0, false
}
logicalBlockSize := uint64(512)
if lbsStr, ok := readStringFileOK(lbsPath); ok {
if parsed, err := strconv.ParseUint(lbsStr, 10, 64); err == nil && parsed > 0 {
logicalBlockSize = parsed
}
}
return sectors * logicalBlockSize, true
}

100
agent/mdraid_linux_test.go Normal file
View File

@@ -0,0 +1,100 @@
//go:build linux
package agent
import (
"os"
"path/filepath"
"testing"
"github.com/henrygd/beszel/internal/entities/smart"
)
func TestMdraidMockSysfsScanAndCollect(t *testing.T) {
tmp := t.TempDir()
prev := mdraidSysfsRoot
mdraidSysfsRoot = tmp
t.Cleanup(func() { mdraidSysfsRoot = prev })
mdDir := filepath.Join(tmp, "block", "md0", "md")
queueDir := filepath.Join(tmp, "block", "md0", "queue")
if err := os.MkdirAll(mdDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(queueDir, 0o755); err != nil {
t.Fatal(err)
}
write := func(path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write(filepath.Join(mdDir, "array_state"), "active\n")
write(filepath.Join(mdDir, "level"), "raid1\n")
write(filepath.Join(mdDir, "raid_disks"), "2\n")
write(filepath.Join(mdDir, "degraded"), "0\n")
write(filepath.Join(mdDir, "sync_action"), "resync\n")
write(filepath.Join(mdDir, "sync_completed"), "10%\n")
write(filepath.Join(mdDir, "sync_speed"), "100M\n")
write(filepath.Join(mdDir, "mismatch_cnt"), "0\n")
write(filepath.Join(queueDir, "logical_block_size"), "512\n")
write(filepath.Join(tmp, "block", "md0", "size"), "2048\n")
devs := scanMdraidDevices()
if len(devs) != 1 {
t.Fatalf("scanMdraidDevices() = %d devices, want 1", len(devs))
}
if devs[0].Name != "/dev/md0" || devs[0].Type != "mdraid" {
t.Fatalf("scanMdraidDevices()[0] = %+v, want Name=/dev/md0 Type=mdraid", devs[0])
}
sm := &SmartManager{SmartDataMap: map[string]*smart.SmartData{}}
ok, err := sm.collectMdraidHealth(devs[0])
if err != nil || !ok {
t.Fatalf("collectMdraidHealth() = (ok=%v, err=%v), want (true,nil)", ok, err)
}
if len(sm.SmartDataMap) != 1 {
t.Fatalf("SmartDataMap len=%d, want 1", len(sm.SmartDataMap))
}
var got *smart.SmartData
for _, v := range sm.SmartDataMap {
got = v
break
}
if got == nil {
t.Fatalf("SmartDataMap value nil")
}
if got.DiskType != "mdraid" || got.DiskName != "/dev/md0" {
t.Fatalf("disk fields = (type=%q name=%q), want (mdraid,/dev/md0)", got.DiskType, got.DiskName)
}
if got.SmartStatus != "WARNING" {
t.Fatalf("SmartStatus=%q, want WARNING", got.SmartStatus)
}
if got.ModelName == "" || got.Capacity == 0 {
t.Fatalf("identity fields = (model=%q cap=%d), want non-empty model and cap>0", got.ModelName, got.Capacity)
}
if len(got.Attributes) < 5 {
t.Fatalf("attributes len=%d, want >= 5", len(got.Attributes))
}
}
func TestMdraidSmartStatus(t *testing.T) {
if got := mdraidSmartStatus(mdraidHealth{arrayState: "inactive"}); got != "FAILED" {
t.Fatalf("mdraidSmartStatus(inactive) = %q, want FAILED", got)
}
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", degraded: 1}); got != "FAILED" {
t.Fatalf("mdraidSmartStatus(degraded) = %q, want FAILED", got)
}
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", syncAction: "recover"}); got != "WARNING" {
t.Fatalf("mdraidSmartStatus(recover) = %q, want WARNING", got)
}
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean"}); got != "PASSED" {
t.Fatalf("mdraidSmartStatus(clean) = %q, want PASSED", got)
}
if got := mdraidSmartStatus(mdraidHealth{arrayState: "unknown"}); got != "UNKNOWN" {
t.Fatalf("mdraidSmartStatus(unknown) = %q, want UNKNOWN", got)
}
}

11
agent/mdraid_stub.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !linux
package agent
func scanMdraidDevices() []*DeviceInfo {
return nil
}
func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error) {
return false, nil
}

177
agent/pve.go Normal file
View File

@@ -0,0 +1,177 @@
package agent
import (
"context"
"crypto/tls"
"errors"
"log/slog"
"net/http"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/luthermonson/go-proxmox"
)
type pveManager struct {
client *proxmox.Client // Client to query PVE API
nodeName string // Cluster node name
cpuCount int // CPU count on node
nodeStatsMap map[string]*container.PveNodeStats // Keeps track of pve node stats
lastInitTry time.Time // Last time node initialization was attempted
}
// newPVEManager creates a new PVE manager - may return nil if required environment variables
// are not set or if there is an error connecting to the API
func newPVEManager() *pveManager {
url, exists := GetEnv("PROXMOX_URL")
if !exists {
url = "https://localhost:8006/api2/json"
}
const nodeEnvVar = "PROXMOX_NODE"
const tokenIDEnvVar = "PROXMOX_TOKENID"
const secretEnvVar = "PROXMOX_SECRET"
nodeName, nodeNameExists := GetEnv(nodeEnvVar)
tokenID, tokenIDExists := GetEnv(tokenIDEnvVar)
secret, secretExists := GetEnv(secretEnvVar)
if !nodeNameExists || !tokenIDExists || !secretExists {
slog.Debug("Proxmox env vars unset", nodeEnvVar, nodeNameExists, tokenIDEnvVar, tokenIDExists, secretEnvVar, secretExists)
return nil
}
// PROXMOX_INSECURE_TLS defaults to true; set to "false" to enable TLS verification
insecureTLS := true
if val, exists := GetEnv("PROXMOX_INSECURE_TLS"); exists {
insecureTLS = val != "false"
}
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureTLS,
},
},
}
client := proxmox.NewClient(url,
proxmox.WithHTTPClient(&httpClient),
proxmox.WithAPIToken(tokenID, secret),
)
pveManager := pveManager{
client: client,
nodeName: nodeName,
nodeStatsMap: make(map[string]*container.PveNodeStats),
}
return &pveManager
}
// ensureInitialized checks if the PVE manager is initialized and attempts to initialize it if not.
// It returns an error if initialization fails or if a retry is pending.
func (pm *pveManager) ensureInitialized(ctx context.Context) error {
if pm.client == nil {
return errors.New("PVE client not configured")
}
if pm.cpuCount > 0 {
return nil
}
if time.Since(pm.lastInitTry) < 30*time.Second {
return errors.New("PVE initialization retry pending")
}
pm.lastInitTry = time.Now()
node, err := pm.client.Node(ctx, pm.nodeName)
if err != nil {
return err
}
if node.CPUInfo.CPUs <= 0 {
return errors.New("node returned zero CPUs")
}
pm.cpuCount = node.CPUInfo.CPUs
return nil
}
// getPVEStats returns stats for all running VMs/LXCs
func (pm *pveManager) getPVEStats() ([]*container.PveNodeStats, error) {
if err := pm.ensureInitialized(context.Background()); err != nil {
slog.Warn("Proxmox API unavailable", "err", err)
return nil, err
}
cluster, err := pm.client.Cluster(context.Background())
if err != nil {
slog.Error("Error getting cluster", "err", err)
return nil, err
}
resources, err := cluster.Resources(context.Background(), "vm")
if err != nil {
slog.Error("Error getting resources", "err", err, "resources", resources)
return nil, err
}
containersLength := len(resources)
resourceIds := make(map[string]struct{}, containersLength)
// only include running vms and lxcs on selected node
for _, resource := range resources {
if resource.Node == pm.nodeName && resource.Status == "running" {
resourceIds[resource.ID] = struct{}{}
}
}
// remove invalid container stats
for id := range pm.nodeStatsMap {
if _, exists := resourceIds[id]; !exists {
delete(pm.nodeStatsMap, id)
}
}
// populate stats
stats := make([]*container.PveNodeStats, 0, len(resourceIds))
for _, resource := range resources {
if _, exists := resourceIds[resource.ID]; !exists {
continue
}
resourceStats, initialized := pm.nodeStatsMap[resource.ID]
if !initialized {
resourceStats = &container.PveNodeStats{}
pm.nodeStatsMap[resource.ID] = resourceStats
}
resourceStats.Name = resource.Name
resourceStats.Id = resource.ID
resourceStats.Type = resource.Type
resourceStats.MaxCPU = resource.MaxCPU
resourceStats.MaxMem = resource.MaxMem
resourceStats.Uptime = resource.Uptime
resourceStats.DiskRead = resource.DiskRead
resourceStats.DiskWrite = resource.DiskWrite
resourceStats.Disk = resource.MaxDisk
// prevent first run from sending all prev sent/recv bytes
total_sent := resource.NetOut
total_recv := resource.NetIn
var sent_delta, recv_delta float64
if initialized {
secondsElapsed := time.Since(resourceStats.PrevReadTime).Seconds()
if secondsElapsed > 0 {
sent_delta = float64(total_sent-resourceStats.PrevNet.Sent) / secondsElapsed
recv_delta = float64(total_recv-resourceStats.PrevNet.Recv) / secondsElapsed
}
}
resourceStats.PrevNet.Sent = total_sent
resourceStats.PrevNet.Recv = total_recv
resourceStats.PrevReadTime = time.Now()
// Update final stats values
resourceStats.Cpu = twoDecimals(100.0 * resource.CPU * float64(resource.MaxCPU) / float64(pm.cpuCount))
resourceStats.Mem = bytesToMegabytes(float64(resource.Mem))
resourceStats.Bandwidth = [2]uint64{uint64(sent_delta), uint64(recv_delta)}
resourceStats.NetOut = total_sent
resourceStats.NetIn = total_recv
stats = append(stats, resourceStats)
}
return stats, nil
}

92
agent/pve_test.go Normal file
View File

@@ -0,0 +1,92 @@
package agent
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/luthermonson/go-proxmox"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewPVEManagerDoesNotConnectAtStartup(t *testing.T) {
t.Setenv("BESZEL_AGENT_PROXMOX_URL", "https://127.0.0.1:1/api2/json")
t.Setenv("BESZEL_AGENT_PROXMOX_NODE", "pve")
t.Setenv("BESZEL_AGENT_PROXMOX_TOKENID", "root@pam!test")
t.Setenv("BESZEL_AGENT_PROXMOX_SECRET", "secret")
pm := newPVEManager()
require.NotNil(t, pm)
assert.Zero(t, pm.cpuCount)
}
func TestPVEManagerRetriesInitialization(t *testing.T) {
var nodeRequests atomic.Int32
var clusterRequests atomic.Int32
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api2/json/nodes/pve/status":
nodeRequests.Add(1)
fmt.Fprint(w, `{"data":{"cpuinfo":{"cpus":8}}}`)
case "/api2/json/cluster/status":
fmt.Fprint(w, `{"data":[{"type":"cluster","name":"test-cluster","id":"test-cluster","version":1,"quorate":1}]}`)
case "/api2/json/cluster/resources":
clusterRequests.Add(1)
fmt.Fprint(w, `{"data":[{"id":"qemu/101","type":"qemu","node":"pve","status":"running","name":"vm-101","cpu":0.5,"maxcpu":4,"maxmem":4096,"mem":2048,"netin":1024,"netout":2048,"diskread":10,"diskwrite":20,"maxdisk":8192,"uptime":60}]}`)
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer server.Close()
pm := &pveManager{
client: proxmox.NewClient(server.URL+"/api2/json",
proxmox.WithHTTPClient(&http.Client{
Transport: &failOnceRoundTripper{
base: server.Client().Transport,
},
}),
proxmox.WithAPIToken("root@pam!test", "secret"),
),
nodeName: "pve",
nodeStatsMap: make(map[string]*container.PveNodeStats),
}
stats, err := pm.getPVEStats()
require.Error(t, err)
assert.Nil(t, stats)
assert.Zero(t, pm.cpuCount)
pm.lastInitTry = time.Now().Add(-31 * time.Second)
stats, err = pm.getPVEStats()
require.NoError(t, err)
require.Len(t, stats, 1)
assert.Equal(t, int32(1), nodeRequests.Load())
assert.Equal(t, int32(1), clusterRequests.Load())
assert.Equal(t, 8, pm.cpuCount)
assert.Equal(t, "qemu/101", stats[0].Id)
assert.Equal(t, 25.0, stats[0].Cpu)
assert.Equal(t, uint64(1024), stats[0].NetIn)
assert.Equal(t, uint64(2048), stats[0].NetOut)
}
type failOnceRoundTripper struct {
base http.RoundTripper
failed atomic.Bool
}
func (rt *failOnceRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Path == "/api2/json/nodes/pve/status" && !rt.failed.Swap(true) {
return nil, errors.New("dial tcp 127.0.0.1:8006: connect: connection refused")
}
return rt.base.RoundTrip(req)
}
var _ http.RoundTripper = (*failOnceRoundTripper)(nil)

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent
@@ -560,6 +559,10 @@ func TestWriteToSessionEncoding(t *testing.T) {
// Helper function to create test data for encoding tests
func createTestCombinedData() *system.CombinedData {
var stats = container.Stats{}
stats.Name = "test-container"
stats.Cpu = 10.5
stats.Mem = 1073741824 // 1GB
return &system.CombinedData{
Stats: system.Stats{
Cpu: 25.5,
@@ -578,11 +581,7 @@ func createTestCombinedData() *system.CombinedData {
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{
{
Name: "test-container",
Cpu: 10.5,
Mem: 1073741824, // 1GB
},
&stats,
},
}
}

View File

@@ -199,6 +199,13 @@ func (sm *SmartManager) ScanDevices(force bool) error {
hasValidScan = true
}
// Add Linux mdraid arrays by reading sysfs health fields. This does not
// require smartctl and does not scan the whole device.
if raidDevices := scanMdraidDevices(); len(raidDevices) > 0 {
scannedDevices = append(scannedDevices, raidDevices...)
hasValidScan = true
}
finalDevices := mergeDeviceLists(currentDevices, scannedDevices, configuredDevices)
finalDevices = sm.filterExcludedDevices(finalDevices)
sm.updateSmartDevices(finalDevices)
@@ -450,6 +457,12 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
return errNoValidSmartData
}
// mdraid health is not exposed via SMART; Linux exposes array state in sysfs.
if deviceInfo != nil {
if ok, err := sm.collectMdraidHealth(deviceInfo); ok {
return err
}
}
// eMMC health is not exposed via SMART on Linux, but the kernel provides
// wear / EOL indicators via sysfs. Prefer that path when available.
if deviceInfo != nil {
@@ -476,7 +489,7 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
output, err := cmd.CombinedOutput()
// Check if device is in standby (exit status 2)
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 2 {
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 2 {
if hasExistingData {
// Device is in standby and we have cached data, keep using cache
return nil
@@ -1146,9 +1159,11 @@ func NewSmartManager() (*SmartManager, error) {
slog.Debug("smartctl", "path", path, "err", err)
if err != nil {
// Keep the previous fail-fast behavior unless this Linux host exposes
// eMMC health via sysfs, in which case smartctl is optional.
if runtime.GOOS == "linux" && len(scanEmmcDevices()) > 0 {
return sm, nil
// eMMC or mdraid health via sysfs, in which case smartctl is optional.
if runtime.GOOS == "linux" {
if len(scanEmmcDevices()) > 0 || len(scanMdraidDevices()) > 0 {
return sm, nil
}
}
return nil, err
}

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package agent

View File

@@ -7,12 +7,12 @@ import (
"log/slog"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery"
"github.com/henrygd/beszel/agent/zfs"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
@@ -107,7 +107,7 @@ func (a *Agent) refreshSystemDetails() {
}
// zfs
if _, err := getARCSize(); err != nil {
if _, err := zfs.ARCSize(); err != nil {
slog.Debug("Not monitoring ZFS ARC", "err", err)
} else {
a.zfs = true
@@ -178,7 +178,7 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
// }
// subtract ZFS ARC size from used memory and add as its own category
if a.zfs {
if arcSize, _ := getARCSize(); arcSize > 0 && arcSize < v.Used {
if arcSize, _ := zfs.ARCSize(); arcSize > 0 && arcSize < v.Used {
v.Used = v.Used - arcSize
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
systemStats.MemZfsArc = bytesToGigabytes(arcSize)
@@ -250,32 +250,6 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
return systemStats
}
// Returns the size of the ZFS ARC memory cache in bytes
func getARCSize() (uint64, error) {
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
if err != nil {
return 0, err
}
defer file.Close()
// Scan the lines
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "size") {
// Example line: size 4 15032385536
fields := strings.Fields(line)
if len(fields) < 3 {
return 0, err
}
// Return the size as uint64
return strconv.ParseUint(fields[2], 10, 64)
}
}
return 0, fmt.Errorf("failed to parse size field")
}
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
func getOsPrettyName() (string, error) {
file, err := os.Open("/etc/os-release")

11
agent/zfs/zfs_freebsd.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build freebsd
package zfs
import (
"golang.org/x/sys/unix"
)
func ARCSize() (uint64, error) {
return unix.SysctlUint64("kstat.zfs.misc.arcstats.size")
}

34
agent/zfs/zfs_linux.go Normal file
View File

@@ -0,0 +1,34 @@
//go:build linux
// Package zfs provides functions to read ZFS statistics.
package zfs
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func ARCSize() (uint64, error) {
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
if err != nil {
return 0, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "size") {
fields := strings.Fields(line)
if len(fields) < 3 {
return 0, fmt.Errorf("unexpected arcstats size format: %s", line)
}
return strconv.ParseUint(fields[2], 10, 64)
}
}
return 0, fmt.Errorf("size field not found in arcstats")
}

View File

@@ -0,0 +1,9 @@
//go:build !linux && !freebsd
package zfs
import "errors"
func ARCSize() (uint64, error) {
return 0, errors.ErrUnsupported
}

View File

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

7
go.mod
View File

@@ -10,6 +10,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0
github.com/luthermonson/go-proxmox v0.4.0
github.com/lxzan/gws v1.8.9
github.com/nicholas-fedor/shoutrrr v0.13.2
github.com/pocketbase/dbx v1.12.0
@@ -28,8 +29,11 @@ require (
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -41,9 +45,12 @@ require (
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/copier v0.3.4 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect

34
go.sum
View File

@@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
@@ -9,6 +11,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -17,8 +21,12 @@ 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/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
@@ -27,6 +35,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -51,6 +61,8 @@ github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtS
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -62,6 +74,12 @@ github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/v
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -69,6 +87,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/copier v0.3.4 h1:mfU6jI9PtCeUjkjQ322dlff9ELjGDu975C2p/nrubVI=
github.com/jinzhu/copier v0.3.4/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -77,8 +97,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs=
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -91,6 +115,10 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -107,6 +135,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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=
@@ -122,6 +152,8 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -148,6 +180,8 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package alerts_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package alerts_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package alerts_test

View File

@@ -38,7 +38,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
case "Memory":
val = data.Info.MemPct
case "Bandwidth":
val = data.Info.Bandwidth
val = float64(data.Info.BandwidthBytes) / (1024 * 1024)
unit = " MB/s"
case "Disk":
maxUsedPct := data.Info.DiskPct

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package alerts_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package alerts

View File

@@ -127,22 +127,43 @@ var DockerHealthStrings = map[string]DockerHealth{
"unhealthy": DockerHealthUnhealthy,
}
// Docker container stats
type Stats struct {
Name string `json:"n" cbor:"0,keyasint"`
Cpu float64 `json:"c" cbor:"1,keyasint"`
Mem float64 `json:"m" cbor:"2,keyasint"`
NetworkSent float64 `json:"ns,omitzero" cbor:"3,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
NetworkRecv float64 `json:"nr,omitzero" cbor:"4,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"9,keyasint,omitzero"` // [sent bytes, recv bytes]
Health DockerHealth `json:"-" cbor:"5,keyasint"`
Status string `json:"-" cbor:"6,keyasint"`
Id string `json:"-" cbor:"7,keyasint"`
Image string `json:"-" cbor:"8,keyasint"`
// PrevCpu [2]uint64 `json:"-"`
CpuSystem uint64 `json:"-"`
CpuContainer uint64 `json:"-"`
// SharedCoreMetrics contains fields that are common to both container Stats and PveNodeStats
type SharedCoreMetrics struct {
Name string `json:"n" cbor:"0,keyasint"`
Cpu float64 `json:"c" cbor:"1,keyasint"`
Mem float64 `json:"m" cbor:"2,keyasint"`
NetworkSent float64 `json:"ns,omitzero" cbor:"3,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
NetworkRecv float64 `json:"nr,omitzero" cbor:"4,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
Id string `json:"-" cbor:"7,keyasint"`
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"9,keyasint,omitzero"` // [sent bytes, recv bytes]
PrevNet prevNetStats `json:"-"`
PrevReadTime time.Time `json:"-"`
}
// Stats holds data specific to docker containers for the containers table
type Stats struct {
SharedCoreMetrics // used to populate stats field in container_stats
// fields used for containers table
Health DockerHealth `json:"-" cbor:"5,keyasint"`
Status string `json:"-" cbor:"6,keyasint"`
Image string `json:"-" cbor:"8,keyasint"`
}
// PveNodeStats holds data specific to PVE nodes for the pve_vms table
type PveNodeStats struct {
SharedCoreMetrics // used to populate stats field in pve_stats
// fields used for pve_vms table
MaxCPU uint64 `json:"-" cbor:"10,keyasint,omitzero"` // PVE: max vCPU count
MaxMem uint64 `json:"-" cbor:"11,keyasint,omitzero"` // PVE: max memory bytes
Uptime uint64 `json:"-" cbor:"12,keyasint,omitzero"` // PVE: uptime in seconds
Type string `json:"-" cbor:"13,keyasint,omitzero"` // PVE: resource type (e.g. "qemu" or "lxc")
DiskRead uint64 `json:"-" cbor:"14,keyasint,omitzero"` // PVE: cumulative disk read bytes
DiskWrite uint64 `json:"-" cbor:"15,keyasint,omitzero"` // PVE: cumulative disk write bytes
Disk uint64 `json:"-" cbor:"16,keyasint,omitzero"` // PVE: allocated disk size in bytes
NetOut uint64 `json:"-" cbor:"17,keyasint,omitzero"` // PVE: cumulative bytes sent by VM
NetIn uint64 `json:"-" cbor:"18,keyasint,omitzero"` // PVE: cumulative bytes received by VM
}

View File

@@ -170,9 +170,10 @@ type Details struct {
// Final data structure to return to the hub
type CombinedData struct {
Stats Stats `json:"stats" cbor:"0,keyasint"`
Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
Details *Details `cbor:"4,keyasint,omitempty"`
Stats Stats `json:"stats" cbor:"0,keyasint"`
Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
Details *Details `cbor:"4,keyasint,omitempty"`
PVEStats []*container.PveNodeStats `json:"pve,omitempty" cbor:"5,keyasint,omitempty"`
}

View File

@@ -34,7 +34,7 @@ func ColorPrint(color, text string) {
fmt.Println(color + text + colorReset)
}
func ColorPrintf(color, format string, args ...interface{}) {
func ColorPrintf(color, format string, args ...any) {
fmt.Printf(color+format+colorReset+"\n", args...)
}

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package hub
@@ -10,6 +9,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -35,6 +35,26 @@ func createTestHub(t testing.TB) (*Hub, *pbtests.TestApp, error) {
return NewHub(testApp), testApp, nil
}
// cleanupTestHub stops background system goroutines before tearing down the app.
func cleanupTestHub(hub *Hub, testApp *pbtests.TestApp) {
if hub != nil {
sm := hub.GetSystemManager()
sm.RemoveAllSystems()
// Give updater goroutines a brief window to observe cancellation before DB teardown.
for range 20 {
if sm.GetSystemCount() == 0 {
break
}
runtime.Gosched()
time.Sleep(5 * time.Millisecond)
}
time.Sleep(20 * time.Millisecond)
}
if testApp != nil {
testApp.Cleanup()
}
}
// Helper function to create a test record
func createTestRecord(app core.App, collection string, data map[string]any) (*core.Record, error) {
col, err := app.FindCachedCollectionByNameOrId(collection)
@@ -64,7 +84,7 @@ func TestValidateAgentHeaders(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
testCases := []struct {
name string
@@ -145,7 +165,7 @@ func TestGetAllFingerprintRecordsByToken(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// create test user
userRecord, err := createTestUser(testApp)
@@ -235,7 +255,7 @@ func TestSetFingerprint(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Create test user
userRecord, err := createTestUser(testApp)
@@ -315,7 +335,7 @@ func TestCreateSystemFromAgentData(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Create test user
userRecord, err := createTestUser(testApp)
@@ -425,7 +445,7 @@ func TestUniversalTokenFlow(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(nil, testApp)
// Create test user
userRecord, err := createTestUser(testApp)
@@ -493,7 +513,7 @@ func TestAgentConnect(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Create test user
userRecord, err := createTestUser(testApp)
@@ -652,7 +672,7 @@ func TestHandleAgentConnect(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Create test user
userRecord, err := createTestUser(testApp)
@@ -737,7 +757,7 @@ func TestAgentWebSocketIntegration(t *testing.T) {
// Create hub and test app
hub, testApp, err := createTestHub(t)
require.NoError(t, err)
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Get the hub's SSH key
hubSigner, err := hub.GetSSHKey("")
@@ -942,6 +962,8 @@ func TestAgentWebSocketIntegration(t *testing.T) {
}
}
time.Sleep(20 * time.Millisecond)
// Verify fingerprint state by re-reading the specific record
updatedFingerprintRecord, err := testApp.FindRecordById("fingerprints", fingerprintRecord.Id)
require.NoError(t, err)
@@ -976,7 +998,7 @@ func TestMultipleSystemsWithSameUniversalToken(t *testing.T) {
// Create hub and test app
hub, testApp, err := createTestHub(t)
require.NoError(t, err)
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Get the hub's SSH key
hubSigner, err := hub.GetSSHKey("")
@@ -1144,6 +1166,8 @@ func TestMultipleSystemsWithSameUniversalToken(t *testing.T) {
assert.Equal(t, systemCount, systemsAfterCount, "Total system count should remain the same")
}
time.Sleep(20 * time.Millisecond)
// Verify that a fingerprint record exists for this fingerprint
fingerprints, err := testApp.FindRecordsByFilter("fingerprints", "token = {:token} && fingerprint = {:fingerprint}", "", -1, 0, map[string]any{
"token": universalToken,
@@ -1176,7 +1200,7 @@ func TestPermanentUniversalTokenFromDB(t *testing.T) {
// Create hub and test app
hub, testApp, err := createTestHub(t)
require.NoError(t, err)
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Get the hub's SSH key
hubSigner, err := hub.GetSSHKey("")
@@ -1273,7 +1297,7 @@ verify:
func TestFindOrCreateSystemForToken(t *testing.T) {
hub, testApp, err := createTestHub(t)
require.NoError(t, err)
defer testApp.Cleanup()
defer cleanupTestHub(hub, testApp)
// Create test user
userRecord, err := createTestUser(testApp)

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package config_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package expirymap

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package heartbeat_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package hub_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package hub

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"hash/fnv"
"log/slog"
"math/rand"
"net"
"strings"
@@ -209,6 +210,28 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
}
}
// add pve_vms and pve_stats records
if len(data.PVEStats) > 0 {
if data.PVEStats[0].Id != "" {
if err := createPVEVMRecords(txApp, data.PVEStats, sys.Id); err != nil {
slog.Error("Error creating PVE VM records", "err", err)
return err
}
}
pveStatsCollection, err := txApp.FindCachedCollectionByNameOrId("pve_stats")
if err != nil {
return err
}
pveStatsRecord := core.NewRecord(pveStatsCollection)
pveStatsRecord.Set("system", systemRecord.Id)
pveStatsRecord.Set("stats", data.PVEStats)
pveStatsRecord.Set("type", "1m")
if err := txApp.SaveNoValidate(pveStatsRecord); err != nil {
slog.Error("Error creating PVE stats records", "err", err)
return err
}
}
// add new systemd_stats record
if len(data.SystemdServices) > 0 {
if err := createSystemdStatsRecords(txApp, data.SystemdServices, sys.Id); err != nil {
@@ -331,8 +354,43 @@ func createContainerRecords(app core.App, data []*container.Stats, systemId stri
return err
}
// createPVEVMRecords creates or updates pve_vms records
func createPVEVMRecords(app core.App, data []*container.PveNodeStats, systemId string) error {
if len(data) == 0 {
return nil
}
// shared params for all records
params := dbx.Params{
"system": systemId,
"updated": time.Now().UTC().UnixMilli(),
}
valueStrings := make([]string, 0, len(data))
for i, vm := range data {
suffix := fmt.Sprintf("%d", i)
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:type%[1]s}, {:cpu%[1]s}, {:mem%[1]s}, {:netout%[1]s}, {:netin%[1]s}, {:maxcpu%[1]s}, {:maxmem%[1]s}, {:uptime%[1]s}, {:diskread%[1]s}, {:diskwrite%[1]s}, {:disk%[1]s}, {:updated})", suffix))
params["id"+suffix] = makeStableHashId(systemId, vm.Id)
params["name"+suffix] = vm.Name
params["type"+suffix] = vm.Type // "qemu" or "lxc"
params["cpu"+suffix] = vm.Cpu
params["mem"+suffix] = vm.Mem
params["maxcpu"+suffix] = vm.MaxCPU
params["maxmem"+suffix] = vm.MaxMem
params["uptime"+suffix] = vm.Uptime
params["diskread"+suffix] = vm.DiskRead
params["diskwrite"+suffix] = vm.DiskWrite
params["disk"+suffix] = vm.Disk
params["netout"+suffix] = vm.NetOut // cumulative bytes sent by VM
params["netin"+suffix] = vm.NetIn // cumulative bytes received by VM
}
queryString := fmt.Sprintf(
"INSERT INTO pve_vms (id, system, name, type, cpu, mem, netout, netin, maxcpu, maxmem, uptime, diskread, diskwrite, disk, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system=excluded.system, name=excluded.name, type=excluded.type, cpu=excluded.cpu, mem=excluded.mem, netout=excluded.netout, netin=excluded.netin, maxcpu=excluded.maxcpu, maxmem=excluded.maxmem, uptime=excluded.uptime, diskread=excluded.diskread, diskwrite=excluded.diskwrite, disk=excluded.disk, updated=excluded.updated",
strings.Join(valueStrings, ","),
)
_, err := app.DB().NewQuery(queryString).Bind(params).Execute()
return err
}
// 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)
if err != nil || record == nil {

View File

@@ -1,5 +1,4 @@
//go:build !testing
// +build !testing
package systems

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package systems_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package systems

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package ws

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package ws

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package ws

View File

@@ -1685,6 +1685,300 @@ func init() {
"type": "base",
"updateRule": null,
"viewRule": null
},
{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-z0-9]{10}",
"hidden": false,
"id": "text3208210256",
"max": 10,
"min": 10,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "2hz5ncl8tizk5nx",
"hidden": false,
"id": "pve_stats_sys01",
"maxSelect": 1,
"minSelect": 0,
"name": "system",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "pve_stats_json1",
"maxSize": 2000000,
"name": "stats",
"presentable": false,
"required": true,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "pve_stats_type1",
"maxSelect": 1,
"name": "type",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"1m",
"10m",
"20m",
"120m",
"480m"
]
},
{
"hidden": false,
"id": "pve_stats_crt1",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "pve_stats_upd1",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pvestats",
"indexes": [
"CREATE INDEX ` + "`" + `idx_pve_stats_sys_type_created` + "`" + ` ON ` + "`" + `pve_stats` + "`" + ` (\n ` + "`" + `system` + "`" + `,\n ` + "`" + `type` + "`" + `,\n ` + "`" + `created` + "`" + `\n)"
],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"name": "pve_stats",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
},
{
"createRule": null,
"deleteRule": null,
"fields": [
{
"autogeneratePattern": "[a-f0-9]{8}",
"hidden": false,
"id": "text3208210256",
"max": 8,
"min": 8,
"name": "id",
"pattern": "^[a-f0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "2hz5ncl8tizk5nx",
"hidden": false,
"id": "pve_vms_sys001",
"maxSelect": 1,
"minSelect": 0,
"name": "system",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "pve_vms_name01",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "pve_vms_type01",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "pve_vms_cpu001",
"max": 100,
"min": 0,
"name": "cpu",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1253106325",
"max": null,
"min": null,
"name": "maxcpu",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "pve_vms_mem001",
"max": null,
"min": 0,
"name": "mem",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1693954525",
"max": null,
"min": null,
"name": "maxmem",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number208985346",
"max": null,
"min": null,
"name": "disk",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number4125810518",
"max": null,
"min": null,
"name": "diskread",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number752404475",
"max": null,
"min": null,
"name": "diskwrite",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1880667380",
"max": null,
"min": null,
"name": "netout",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2702533949",
"max": null,
"min": null,
"name": "netin",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1563400775",
"max": null,
"min": null,
"name": "uptime",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "pve_vms_upd001",
"max": null,
"min": null,
"name": "updated",
"onlyInt": true,
"presentable": false,
"required": true,
"system": false,
"type": "number"
}
],
"id": "pvevms",
"indexes": [
"CREATE INDEX ` + "`" + `idx_pve_vms_updated` + "`" + ` ON ` + "`" + `pve_vms` + "`" + ` (` + "`" + `updated` + "`" + `)",
"CREATE INDEX ` + "`" + `idx_pve_vms_system` + "`" + ` ON ` + "`" + `pve_vms` + "`" + ` (` + "`" + `system` + "`" + `)"
],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"name": "pve_vms",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": null
}
]`

View File

@@ -42,11 +42,11 @@ type StatsRecord struct {
// global variables for reusing allocations
var (
statsRecord StatsRecord
containerStats []container.Stats
containerStats []container.SharedCoreMetrics
sumStats system.Stats
tempStats system.Stats
queryParams = make(dbx.Params, 1)
containerSums = make(map[string]*container.Stats)
containerSums = make(map[string]*container.SharedCoreMetrics)
)
// Create longer records by averaging shorter records
@@ -82,7 +82,7 @@ func (rm *RecordManager) CreateLongerRecords() {
// wrap the operations in a transaction
rm.app.RunInTransaction(func(txApp core.App) error {
var err error
collections := [2]*core.Collection{}
collections := [3]*core.Collection{}
collections[0], err = txApp.FindCachedCollectionByNameOrId("system_stats")
if err != nil {
return err
@@ -91,6 +91,10 @@ func (rm *RecordManager) CreateLongerRecords() {
if err != nil {
return err
}
collections[2], err = txApp.FindCachedCollectionByNameOrId("pve_stats")
if err != nil {
return err
}
var systems RecordIds
db := txApp.DB()
@@ -150,8 +154,9 @@ func (rm *RecordManager) CreateLongerRecords() {
case "system_stats":
longerRecord.Set("stats", rm.AverageSystemStats(db, recordIds))
case "container_stats":
longerRecord.Set("stats", rm.AverageContainerStats(db, recordIds))
longerRecord.Set("stats", rm.AverageContainerStats(db, recordIds, "container_stats"))
case "pve_stats":
longerRecord.Set("stats", rm.AverageContainerStats(db, recordIds, "pve_stats"))
}
if err := txApp.SaveNoValidate(longerRecord); err != nil {
log.Println("failed to save longer record", "err", err)
@@ -435,8 +440,8 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
return sum
}
// Calculate the average stats of a list of container_stats records
func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds) []container.Stats {
// Calculate the average stats of a list of container_stats or pve_stats records
func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds, collectionName string) []container.SharedCoreMetrics {
// Clear global map for reuse
for k := range containerSums {
delete(containerSums, k)
@@ -453,15 +458,15 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
containerStats = nil
queryParams["id"] = id
db.NewQuery("SELECT stats FROM container_stats WHERE id = {:id}").Bind(queryParams).One(&statsRecord)
db.NewQuery(fmt.Sprintf("SELECT stats FROM %s WHERE id = {:id}", collectionName)).Bind(queryParams).One(&statsRecord)
if err := json.Unmarshal(statsRecord.Stats, &containerStats); err != nil {
return []container.Stats{}
return []container.SharedCoreMetrics{}
}
for i := range containerStats {
stat := containerStats[i]
if _, ok := sums[stat.Name]; !ok {
sums[stat.Name] = &container.Stats{Name: stat.Name}
sums[stat.Name] = &container.SharedCoreMetrics{Name: stat.Name}
}
sums[stat.Name].Cpu += stat.Cpu
sums[stat.Name].Mem += stat.Mem
@@ -476,9 +481,9 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
}
}
result := make([]container.Stats, 0, len(sums))
result := make([]container.SharedCoreMetrics, 0, len(sums))
for _, value := range sums {
result = append(result, container.Stats{
result = append(result, container.SharedCoreMetrics{
Name: value.Name,
Cpu: twoDecimals(value.Cpu / count),
Mem: twoDecimals(value.Mem / count),
@@ -499,6 +504,10 @@ func (rm *RecordManager) DeleteOldRecords() {
if err != nil {
return err
}
err = deleteOldPVEVMRecords(txApp)
if err != nil {
return err
}
err = deleteOldSystemdServiceRecords(txApp)
if err != nil {
return err
@@ -537,7 +546,7 @@ func deleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int)
// Deletes system_stats records older than what is displayed in the UI
func deleteOldSystemStats(app core.App) error {
// Collections to process
collections := [2]string{"system_stats", "container_stats"}
collections := [3]string{"system_stats", "container_stats", "pve_stats"}
// Record types and their retention periods
type RecordDeletionData struct {
@@ -590,6 +599,19 @@ func deleteOldSystemdServiceRecords(app core.App) error {
return nil
}
// Deletes pve_vms records that haven't been updated in the last 10 minutes
func deleteOldPVEVMRecords(app core.App) error {
now := time.Now().UTC()
tenMinutesAgo := now.Add(-10 * time.Minute)
_, err := app.DB().NewQuery("DELETE FROM pve_vms WHERE updated < {:updated}").Bind(dbx.Params{"updated": tenMinutesAgo.UnixMilli()}).Execute()
if err != nil {
return fmt.Errorf("failed to delete old pve_vms records: %v", err)
}
return nil
}
// Deletes container records that haven't been updated in the last 10 minutes
func deleteOldContainerRecords(app core.App) error {
now := time.Now().UTC()

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package records_test

View File

@@ -1,5 +1,4 @@
//go:build testing
// +build testing
package records

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -12,7 +12,7 @@
"lineWidth": 120,
"formatWithErrors": true
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"assist": { "actions": { "source": { "organizeImports": "off" } } },
"linter": {
"enabled": true,
"rules": {

View File

@@ -1,13 +1,14 @@
{
"name": "beszel",
"private": true,
"version": "0.18.3",
"version": "0.18.4",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "lingui extract --overwrite && lingui compile && vite build",
"preview": "vite preview",
"sync": "lingui extract --overwrite && lingui compile",
"sync_no_compile": "lingui extract --overwrite --clean",
"sync_and_purge": "lingui extract --overwrite --clean && lingui compile",
"format": "biome format --write .",
"lint": "biome lint .",

View File

@@ -26,7 +26,7 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
/>
</Button>
</SheetTrigger>
<SheetContent className="max-h-full overflow-auto w-150 !max-w-full p-4 sm:p-6">
<SheetContent className="max-h-full overflow-auto w-160 !max-w-full p-4 sm:p-6">
{opened && <AlertDialogContent system={system} />}
</SheetContent>
</Sheet>

View File

@@ -7,6 +7,7 @@ import { lazy, memo, Suspense, useMemo, useState } from "react"
import { $router, Link } from "@/components/router"
import { Checkbox } from "@/components/ui/checkbox"
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { toast } from "@/components/ui/use-toast"
@@ -20,7 +21,7 @@ const Slider = lazy(() => import("@/components/ui/slider"))
const endpoint = "/api/beszel/user-alerts"
const alertDebounce = 100
const alertDebounce = 400
const alertKeys = Object.keys(alertInfo) as (keyof typeof alertInfo)[]
@@ -244,7 +245,7 @@ export function AlertContent({
<Suspense fallback={<div className="h-10" />}>
{!singleDescription && (
<div>
<p id={`v${name}`} className="text-sm block h-8">
<p id={`v${name}`} className="text-sm block h-6">
{alertData.invert ? (
<Trans>
Average drops below{" "}
@@ -263,21 +264,38 @@ export function AlertContent({
</Trans>
)}
</p>
<div className="flex gap-3">
<div className="flex gap-3 items-center">
<Slider
aria-labelledby={`v${name}`}
defaultValue={[value]}
value={[value]}
onValueCommit={(val) => sendUpsert(min, val[0])}
onValueChange={(val) => setValue(val[0])}
step={alertData.step ?? 1}
min={alertData.min ?? 1}
max={alertData.max ?? 99}
/>
<Input
type="number"
value={value}
onChange={(e) => {
let val = parseFloat(e.target.value)
if (!Number.isNaN(val)) {
if (alertData.max != null) val = Math.min(val, alertData.max)
if (alertData.min != null) val = Math.max(val, alertData.min)
setValue(val)
sendUpsert(min, val)
}
}}
step={alertData.step ?? 1}
min={alertData.min ?? 1}
max={alertData.max ?? 99}
className="w-16 h-8 text-center px-1"
/>
</div>
</div>
)}
<div className={cn(singleDescription && "col-span-full lowercase")}>
<p id={`t${name}`} className="text-sm block h-8 first-letter:uppercase">
<p id={`t${name}`} className="text-sm block h-6 first-letter:uppercase">
{singleDescription && (
<>
{singleDescription}
@@ -289,15 +307,30 @@ export function AlertContent({
<Plural value={min} one="minute" other="minutes" />
</Trans>
</p>
<div className="flex gap-3">
<div className="flex gap-3 items-center">
<Slider
aria-labelledby={`v${name}`}
defaultValue={[min]}
onValueCommit={(minVal) => sendUpsert(minVal[0], value)}
aria-labelledby={`t${name}`}
value={[min]}
onValueCommit={(val) => sendUpsert(val[0], value)}
onValueChange={(val) => setMin(val[0])}
min={1}
max={60}
/>
<Input
type="number"
value={min}
onChange={(e) => {
let val = parseInt(e.target.value, 10)
if (!Number.isNaN(val)) {
val = Math.max(1, Math.min(val, 60))
setMin(val)
sendUpsert(val, value)
}
}}
min={1}
max={60}
className="w-16 h-8 text-center px-1"
/>
</div>
</div>
</Suspense>

View File

@@ -23,14 +23,16 @@ export default memo(function ContainerChart({
chartType,
chartConfig,
unit = "%",
filterStore = $containerFilter,
}: {
dataKey: string
chartData: ChartData
chartType: ChartType
chartConfig: ChartConfig
unit?: string
filterStore?: typeof $containerFilter
}) {
const filter = useStore($containerFilter)
const filter = useStore(filterStore)
const userSettings = useStore($userSettings)
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -43,7 +43,7 @@ export function copyDockerCompose(port = "45876", publicKey: string, token: stri
export function copyDockerRun(port = "45876", publicKey: string, token: string) {
copyToClipboard(
`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -v ./beszel_agent_data:/var/lib/beszel-agent -e KEY="${publicKey}" -e LISTEN=${port} -e TOKEN="${token}" -e HUB_URL="${getHubURL()}" henrygd/beszel-agent`
`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -v beszel_agent_data:/var/lib/beszel-agent -e KEY="${publicKey}" -e LISTEN=${port} -e TOKEN="${token}" -e HUB_URL="${getHubURL()}" henrygd/beszel-agent`
)
}

View File

@@ -32,7 +32,10 @@ export function LangToggle() {
className={cn("px-2.5 flex gap-2.5 cursor-pointer", lang === i18n.locale && "bg-accent/70 font-medium")}
onClick={() => dynamicActivate(lang)}
>
<span>{e}</span> {label}
<span>
{e || <code className="font-mono bg-muted text-[.65em] w-5 h-4 grid place-items-center">{lang}</code>}
</span>{" "}
{label}
</DropdownMenuItem>
))}
</DropdownMenuContent>

View File

@@ -1,4 +1,5 @@
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { getPagePath } from "@nanostores/router"
import {
ContainerIcon,
@@ -31,6 +32,7 @@ import { Logo } from "./logo"
import { ModeToggle } from "./mode-toggle"
import { $router, basePath, Link, prependBasePath } from "./router"
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
import { ProxmoxIcon } from "./ui/icons"
const CommandPalette = lazy(() => import("./command-palette"))
@@ -77,6 +79,20 @@ export default function Navbar() {
</TooltipTrigger>
<TooltipContent>S.M.A.R.T.</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={getPagePath($router, "proxmox")}
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
aria-label={t`Proxmox`}
>
<ProxmoxIcon className="h-[1.2rem] w-[1.2rem] opacity-90" />
</Link>
</TooltipTrigger>
<TooltipContent>
<Trans>Proxmox</Trans>
</TooltipContent>
</Tooltip>
<LangToggle />
<ModeToggle />
<Tooltip>

View File

@@ -0,0 +1,215 @@
import type { Column, ColumnDef } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { cn, decimalString, formatBytes, hourWithSeconds, toFixedFloat } from "@/lib/utils"
import type { PveVmRecord } from "@/types"
import {
ClockIcon,
CpuIcon,
HardDriveIcon,
MemoryStickIcon,
MonitorIcon,
ServerIcon,
TagIcon,
TimerIcon,
} from "lucide-react"
import { EthernetIcon } from "../ui/icons"
import { Badge } from "../ui/badge"
import { t } from "@lingui/core/macro"
import { $allSystemsById } from "@/lib/stores"
import { useStore } from "@nanostores/react"
/** Format uptime in seconds to a human-readable string */
export function formatUptime(seconds: number): string {
if (seconds < 60) return `${seconds}s`
if (seconds < 3600) {
const m = Math.floor(seconds / 60)
return `${m}m`
}
if (seconds < 86400) {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
return m > 0 ? `${h}h ${m}m` : `${h}h`
}
const d = Math.floor(seconds / 86400)
const h = Math.floor((seconds % 86400) / 3600)
return h > 0 ? `${d}d ${h}h` : `${d}d`
}
export const pveVmCols: ColumnDef<PveVmRecord>[] = [
{
id: "name",
sortingFn: (a, b) => a.original.name.localeCompare(b.original.name),
accessorFn: (record) => record.name,
header: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={MonitorIcon} />,
cell: ({ getValue }) => {
return <span className="ms-1 max-w-48 block truncate">{getValue() as string}</span>
},
},
{
id: "system",
accessorFn: (record) => record.system,
sortingFn: (a, b) => {
const allSystems = $allSystemsById.get()
const systemNameA = allSystems[a.original.system]?.name ?? ""
const systemNameB = allSystems[b.original.system]?.name ?? ""
return systemNameA.localeCompare(systemNameB)
},
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
cell: ({ getValue }) => {
const allSystems = useStore($allSystemsById)
return <span className="ms-1 max-w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
},
},
{
id: "type",
accessorFn: (record) => record.type,
sortingFn: (a, b) => a.original.type.localeCompare(b.original.type),
header: ({ column }) => <HeaderButton column={column} name={t`Type`} Icon={TagIcon} />,
cell: ({ getValue }) => {
const type = getValue() as string
return (
<Badge variant="outline" className="dark:border-white/12 ms-1">
{type}
</Badge>
)
},
},
{
id: "cpu",
accessorFn: (record) => record.cpu,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,
cell: ({ getValue }) => {
const val = getValue() as number
return <span className="ms-1 tabular-nums">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>
},
},
{
id: "mem",
accessorFn: (record) => record.mem,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Memory`} Icon={MemoryStickIcon} />,
cell: ({ getValue }) => {
const val = getValue() as number
const formatted = formatBytes(val, false, undefined, true)
return (
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
)
},
},
{
id: "maxmem",
accessorFn: (record) => record.maxmem,
header: ({ column }) => <HeaderButton column={column} name={t`Max`} Icon={MemoryStickIcon} />,
invertSorting: true,
cell: ({ getValue }) => {
// maxmem is stored in bytes; convert to MB for formatBytes
const formatted = formatBytes(getValue() as number, false, undefined, false)
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
},
},
{
id: "disk",
accessorFn: (record) => record.disk,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Disk`} Icon={HardDriveIcon} />,
cell: ({ getValue }) => {
const formatted = formatBytes(getValue() as number, false, undefined, false)
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
},
},
{
id: "diskread",
accessorFn: (record) => record.diskread,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Read`} Icon={HardDriveIcon} />,
cell: ({ getValue }) => {
const val = getValue() as number
const formatted = formatBytes(val, false, undefined, false)
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
},
},
{
id: "diskwrite",
accessorFn: (record) => record.diskwrite,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Write`} Icon={HardDriveIcon} />,
cell: ({ getValue }) => {
const val = getValue() as number
const formatted = formatBytes(val, false, undefined, false)
return <span className="ms-1 tabular-nums">{`${toFixedFloat(formatted.value, 2)} ${formatted.unit}`}</span>
},
},
{
id: "netin",
accessorFn: (record) => record.netin,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Download`} Icon={EthernetIcon} />,
cell: ({ getValue }) => {
const val = getValue() as number
const formatted = formatBytes(val, false, undefined, false)
return (
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
)
},
},
{
id: "netout",
accessorFn: (record) => record.netout,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Upload`} Icon={EthernetIcon} />,
cell: ({ getValue }) => {
const val = getValue() as number
const formatted = formatBytes(val, false, undefined, false)
return (
<span className="ms-1 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
)
},
},
{
id: "maxcpu",
accessorFn: (record) => record.maxcpu,
header: ({ column }) => <HeaderButton column={column} name="vCPUs" Icon={CpuIcon} />,
invertSorting: true,
cell: ({ getValue }) => {
return <span className="ms-1 tabular-nums">{getValue() as number}</span>
},
},
{
id: "uptime",
accessorFn: (record) => record.uptime,
invertSorting: true,
header: ({ column }) => <HeaderButton column={column} name={t`Uptime`} Icon={TimerIcon} />,
cell: ({ getValue }) => {
return <span className="ms-1">{formatUptime(getValue() as number)}</span>
},
},
{
id: "updated",
invertSorting: true,
accessorFn: (record) => record.updated,
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
cell: ({ getValue }) => {
const timestamp = getValue() as number
return <span className="ms-1 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
},
},
]
function HeaderButton({ column, name, Icon }: { column: Column<PveVmRecord>; name: string; Icon: React.ElementType }) {
const isSorted = column.getIsSorted()
return (
<Button
className={cn(
"h-9 px-3 flex items-center gap-2 duration-50",
isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90"
)}
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{Icon && <Icon className="size-4" />}
{name}
{/* <ArrowUpDownIcon className="size-4" /> */}
</Button>
)
}

View File

@@ -0,0 +1,391 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import {
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
type Row,
type SortingState,
type Table as TableType,
useReactTable,
type VisibilityState,
} from "@tanstack/react-table"
import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual"
import { memo, RefObject, useEffect, useRef, useState } from "react"
import { Input } from "@/components/ui/input"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { pb } from "@/lib/api"
import type { PveVmRecord } from "@/types"
import { pveVmCols, formatUptime } from "@/components/pve-table/pve-table-columns"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { cn, decimalString, formatBytes, useBrowserStorage } from "@/lib/utils"
import { Sheet, SheetTitle, SheetHeader, SheetContent, SheetDescription } from "../ui/sheet"
import { $allSystemsById } from "@/lib/stores"
import { LoaderCircleIcon, XIcon } from "lucide-react"
import { Separator } from "../ui/separator"
import { $router, Link } from "../router"
import { listenKeys } from "nanostores"
import { getPagePath } from "@nanostores/router"
export default function PveTable({ systemId }: { systemId?: string }) {
const loadTime = Date.now()
const [data, setData] = useState<PveVmRecord[] | undefined>(undefined)
const [sorting, setSorting] = useBrowserStorage<SortingState>(
`sort-pve-${systemId ? 1 : 0}`,
[{ id: systemId ? "name" : "system", desc: false }],
sessionStorage
)
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [rowSelection, setRowSelection] = useState({})
const [globalFilter, setGlobalFilter] = useState("")
useEffect(() => {
function fetchData(systemId?: string) {
pb.collection<PveVmRecord>("pve_vms")
.getList(0, 2000, {
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
})
.then(({ items }) => {
if (items.length === 0) {
setData((curItems) => {
if (systemId) {
return curItems?.filter((item) => item.system !== systemId) ?? []
}
return []
})
return
}
setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const vmIds = new Set<string>()
const newItems: PveVmRecord[] = []
for (const item of items) {
if (Math.abs(lastUpdated - item.updated) < 70_000) {
vmIds.add(item.id)
newItems.push(item)
}
}
for (const item of curItems ?? []) {
if (!vmIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
})
})
}
// initial load
fetchData(systemId)
// if no systemId, pull pve vms after every system update
if (!systemId) {
return $allSystemsById.listen((_value, _oldValue, systemId) => {
// exclude initial load of systems
if (Date.now() - loadTime > 500) {
fetchData(systemId)
}
})
}
// if systemId, fetch pve vms after the system is updated
return listenKeys($allSystemsById, [systemId], (_newSystems) => {
fetchData(systemId)
})
}, [])
const table = useReactTable({
data: data ?? [],
columns: pveVmCols.filter((col) => (systemId ? col.id !== "system" : true)),
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
defaultColumn: {
sortUndefined: "last",
size: 100,
minSize: 0,
},
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
globalFilter,
},
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: (row, _columnId, filterValue) => {
const vm = row.original
const systemName = $allSystemsById.get()[vm.system]?.name ?? ""
const id = vm.id ?? ""
const name = vm.name ?? ""
const type = vm.type ?? ""
const searchString = `${systemName} ${id} ${name} ${type}`.toLowerCase()
return (filterValue as string)
.toLowerCase()
.split(" ")
.every((term) => searchString.includes(term))
},
})
const rows = table.getRowModel().rows
const visibleColumns = table.getVisibleLeafColumns()
return (
<Card className="p-6 @container w-full">
<CardHeader className="p-0 mb-4">
<div className="grid md:flex gap-5 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2">
<Trans>Proxmox Resources</Trans>
</CardTitle>
<CardDescription className="flex">
<Trans>CPU is percent of overall host CPU usage.</Trans>
</CardDescription>
</div>
<div className="relative ms-auto w-full max-w-full md:w-64">
<Input
placeholder={t`Filter...`}
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="ps-4 pe-10 w-full"
/>
{globalFilter && (
<button
type="button"
aria-label={t`Clear`}
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 flex items-center justify-center text-muted-foreground hover:text-foreground"
onClick={() => setGlobalFilter("")}
>
<XIcon className="h-4 w-4" />
</button>
)}
</div>
</div>
</CardHeader>
<div className="rounded-md">
<AllPveTable table={table} rows={rows} colLength={visibleColumns.length} data={data} />
</div>
</Card>
)
}
const AllPveTable = memo(function AllPveTable({
table,
rows,
colLength,
data,
}: {
table: TableType<PveVmRecord>
rows: Row<PveVmRecord>[]
colLength: number
data: PveVmRecord[] | undefined
}) {
const scrollRef = useRef<HTMLDivElement>(null)
const activeVm = useRef<PveVmRecord | null>(null)
const [sheetOpen, setSheetOpen] = useState(false)
const openSheet = (vm: PveVmRecord) => {
activeVm.current = vm
setSheetOpen(true)
}
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => 54,
getScrollElement: () => scrollRef.current,
overscan: 5,
})
const virtualRows = virtualizer.getVirtualItems()
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full text-nowrap">
<PveTableHead table={table} />
<TableBody>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]
return <PveTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} />
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
{data ? (
<Trans>No results.</Trans>
) : (
<LoaderCircleIcon className="animate-spin size-10 opacity-60 mx-auto" />
)}
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
<PveVmSheet sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} activeVm={activeVm} />
</div>
)
})
function PveVmSheet({
sheetOpen,
setSheetOpen,
activeVm,
}: {
sheetOpen: boolean
setSheetOpen: (open: boolean) => void
activeVm: RefObject<PveVmRecord | null>
}) {
const vm = activeVm.current
if (!vm) return null
const memFormatted = formatBytes(vm.mem, false, undefined, true)
const maxMemFormatted = formatBytes(vm.maxmem, false, undefined, false)
const netoutFormatted = formatBytes(vm.netout, false, undefined, false)
const netinFormatted = formatBytes(vm.netin, false, undefined, false)
const diskReadFormatted = formatBytes(vm.diskread, false, undefined, false)
const diskWriteFormatted = formatBytes(vm.diskwrite, false, undefined, false)
const diskFormatted = formatBytes(vm.disk, false, undefined, false)
return (
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent className="w-full sm:max-w-120 p-2">
<SheetHeader>
<SheetTitle>{vm.name}</SheetTitle>
<SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1">
<Link className="hover:underline" href={getPagePath($router, "system", { id: vm.system })}>
{$allSystemsById.get()[vm.system]?.name ?? ""}
</Link>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
{vm.type}
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<Trans>Up {formatUptime(vm.uptime)}</Trans>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
{vm.id}
</SheetDescription>
</SheetHeader>
<div className="px-3 pb-3 -mt-2 flex flex-col gap-3">
<h3 className="text-sm font-medium">
<Trans>Details</Trans>
</h3>
<dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<dt className="text-muted-foreground">
<Trans>CPU Usage</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(vm.cpu, vm.cpu >= 10 ? 1 : 2)}%`}</dd>
<dt className="text-muted-foreground">
<Trans>Memory Used</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(memFormatted.value, memFormatted.value >= 10 ? 1 : 2)} ${memFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>Upload</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(netoutFormatted.value, netoutFormatted.value >= 10 ? 1 : 2)} ${netoutFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>Download</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(netinFormatted.value, netinFormatted.value >= 10 ? 1 : 2)} ${netinFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>vCPUs</Trans>
</dt>
<dd className="tabular-nums">{vm.maxcpu}</dd>
<dt className="text-muted-foreground">
<Trans>Max Memory</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(maxMemFormatted.value, maxMemFormatted.value >= 10 ? 1 : 2)} ${maxMemFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>Disk Read</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(diskReadFormatted.value, diskReadFormatted.value >= 10 ? 1 : 2)} ${diskReadFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>Disk Write</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(diskWriteFormatted.value, diskWriteFormatted.value >= 10 ? 1 : 2)} ${diskWriteFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>Disk Size</Trans>
</dt>
<dd className="tabular-nums">{`${decimalString(diskFormatted.value, diskFormatted.value >= 10 ? 1 : 2)} ${diskFormatted.unit}`}</dd>
<dt className="text-muted-foreground">
<Trans>Uptime</Trans>
</dt>
<dd className="tabular-nums">{formatUptime(vm.uptime)}</dd>
</dl>
</div>
</SheetContent>
</Sheet>
)
}
function PveTableHead({ table }: { table: TableType<PveVmRecord> }) {
return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-2" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</tr>
))}
</TableHeader>
)
}
const PveTableRow = memo(function PveTableRow({
row,
virtualRow,
openSheet,
}: {
row: Row<PveVmRecord>
virtualRow: VirtualItem
openSheet: (vm: PveVmRecord) => void
}) {
return (
<TableRow
data-state={row.getIsSelected() && "selected"}
className="cursor-pointer transition-opacity"
onClick={() => openSheet(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="py-0 ps-4.5"
style={{
height: virtualRow.size,
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
})

View File

@@ -3,6 +3,7 @@ import { createRouter } from "@nanostores/router"
const routes = {
home: "/",
containers: "/containers",
proxmox: "/proxmox",
smart: "/smart",
system: `/system/:id`,
settings: `/settings/:name?`,

View File

@@ -0,0 +1,26 @@
import { useLingui } from "@lingui/react/macro"
import { memo, useEffect, useMemo } from "react"
import PveTable from "@/components/pve-table/pve-table"
import { ActiveAlerts } from "@/components/active-alerts"
import { FooterRepoLink } from "@/components/footer-repo-link"
export default memo(() => {
const { t } = useLingui()
useEffect(() => {
document.title = `${t`All Proxmox VMs`} / Beszel`
}, [t])
return useMemo(
() => (
<>
<div className="grid gap-4">
<ActiveAlerts />
<PveTable />
</div>
<FooterRepoLink />
</>
),
[]
)
})

View File

@@ -70,7 +70,16 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<SelectContent>
{languages.map(([lang, label, e]) => (
<SelectItem key={lang} value={lang}>
<span className="me-2.5">{e}</span>
<span className="me-2.5">
{e || (
<code
aria-hidden="true"
className="font-mono bg-muted text-[.65em] w-5 h-4 inline-grid place-items-center"
>
{lang}
</code>
)}
</span>
{label}
</SelectItem>
))}

View File

@@ -1,7 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { redirectPage } from "@nanostores/router"
import clsx from "clsx"
import { LoaderCircleIcon, SendIcon } from "lucide-react"
import { useEffect, useState } from "react"
import { $router } from "@/components/router"
@@ -10,6 +9,7 @@ import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { toast } from "@/components/ui/use-toast"
import { isAdmin, pb } from "@/lib/api"
import { cn } from "@/lib/utils"
interface HeartbeatStatus {
enabled: boolean
@@ -37,10 +37,10 @@ export default function HeartbeatSettings() {
setIsLoading(true)
const res = await pb.send<HeartbeatStatus>("/api/beszel/heartbeat-status", {})
setStatus(res)
} catch (error: any) {
} catch (error: unknown) {
toast({
title: t`Error`,
description: error.message,
description: (error as Error).message,
variant: "destructive",
})
} finally {
@@ -66,10 +66,10 @@ export default function HeartbeatSettings() {
variant: "destructive",
})
}
} catch (error: any) {
} catch (error: unknown) {
toast({
title: t`Error`,
description: error.message,
description: (error as Error).message,
variant: "destructive",
})
} finally {
@@ -77,8 +77,6 @@ export default function HeartbeatSettings() {
}
}
const TestIcon = isTesting ? LoaderCircleIcon : SendIcon
return (
<div>
<div>
@@ -94,107 +92,123 @@ export default function HeartbeatSettings() {
</div>
<Separator className="my-4" />
{isLoading ? (
<div className="flex items-center gap-2 text-muted-foreground py-4">
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
<Trans>Loading heartbeat status...</Trans>
</div>
) : status?.enabled ? (
<div className="space-y-5">
<div className="flex items-center gap-2">
<Badge variant="success">
<Trans>Active</Trans>
</Badge>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<ConfigItem label={t`Endpoint URL`} value={status.url ?? ""} mono />
<ConfigItem label={t`Interval`} value={`${status.interval}s`} />
<ConfigItem label={t`HTTP Method`} value={status.method ?? "POST"} />
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-1">
<Trans>Test heartbeat</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Send a single heartbeat ping to verify your endpoint is working.</Trans>
</p>
<Button
type="button"
variant="outline"
className="flex items-center gap-1.5"
onClick={sendTestHeartbeat}
disabled={isTesting}
>
<TestIcon className={clsx("h-4 w-4", isTesting && "animate-spin")} />
<Trans>Send test heartbeat</Trans>
</Button>
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-2">
<Trans>Payload format</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-2">
<Trans>
When using POST, each heartbeat includes a JSON payload with system status summary, list of down
systems, and triggered alerts.
</Trans>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
The overall status is <code className="bg-muted rounded-sm px-1 text-primary">ok</code> when all systems
are up, <code className="bg-muted rounded-sm px-1 text-primary">warn</code> when alerts are triggered,
and <code className="bg-muted rounded-sm px-1 text-primary">error</code> when any system is down.
</Trans>
</p>
</div>
</div>
{status?.enabled ? (
<EnabledState status={status} isTesting={isTesting} sendTestHeartbeat={sendTestHeartbeat} />
) : (
<div className="grid gap-4">
<div>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</Trans>
</p>
<div className="grid gap-2.5">
<EnvVarItem
name="HEARTBEAT_URL"
description={t`Endpoint URL to ping (required)`}
example="https://uptime.betterstack.com/api/v1/heartbeat/xxxx"
/>
<EnvVarItem name="HEARTBEAT_INTERVAL" description={t`Seconds between pings (default: 60)`} example="60" />
<EnvVarItem
name="HEARTBEAT_METHOD"
description={t`HTTP method: POST, GET, or HEAD (default: POST)`}
example="POST"
/>
</div>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>After setting the environment variables, restart your Beszel hub for changes to take effect.</Trans>
</p>
</div>
<NotEnabledState isLoading={isLoading} />
)}
</div>
)
}
function EnabledState({
status,
isTesting,
sendTestHeartbeat,
}: {
status: HeartbeatStatus
isTesting: boolean
sendTestHeartbeat: () => void
}) {
const TestIcon = isTesting ? LoaderCircleIcon : SendIcon
return (
<div className="space-y-5">
<div className="flex items-center gap-2">
<Badge variant="success">
<Trans>Active</Trans>
</Badge>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<ConfigItem label={t`Endpoint URL`} value={status.url ?? ""} mono />
<ConfigItem label={t`Interval`} value={`${status.interval}s`} />
<ConfigItem label={t`HTTP Method`} value={status.method ?? "POST"} />
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-1">
<Trans>Test heartbeat</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Send a single heartbeat ping to verify your endpoint is working.</Trans>
</p>
<Button
type="button"
variant="outline"
className="flex items-center gap-1.5"
onClick={sendTestHeartbeat}
disabled={isTesting}
>
<TestIcon className={cn("size-4", isTesting && "animate-spin")} />
<Trans>Send test heartbeat</Trans>
</Button>
</div>
<Separator />
<div>
<h4 className="text-base font-medium mb-2">
<Trans>Payload format</Trans>
</h4>
<p className="text-sm text-muted-foreground leading-relaxed mb-2">
<Trans>
When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems,
and triggered alerts.
</Trans>
</p>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
The overall status is <code className="bg-muted rounded-sm px-1 text-primary">ok</code> when all systems are
up, <code className="bg-muted rounded-sm px-1 text-primary">warn</code> when alerts are triggered, and{" "}
<code className="bg-muted rounded-sm px-1 text-primary">error</code> when any system is down.
</Trans>
</p>
</div>
</div>
)
}
function NotEnabledState({ isLoading }: { isLoading?: boolean }) {
return (
<div className={cn("grid gap-4", isLoading && "animate-pulse")}>
<div>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
<Trans>Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</Trans>
</p>
<div className="grid gap-2.5">
<EnvVarItem
name="HEARTBEAT_URL"
description={t`Endpoint URL to ping (required)`}
example="https://uptime.betterstack.com/api/v1/heartbeat/xxxx"
/>
<EnvVarItem name="HEARTBEAT_INTERVAL" description={t`Seconds between pings (default: 60)`} example="60" />
<EnvVarItem
name="HEARTBEAT_METHOD"
description={t`HTTP method: POST, GET, or HEAD (default: POST)`}
example="POST"
/>
</div>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>After setting the environment variables, restart your Beszel hub for changes to take effect.</Trans>
</p>
</div>
)
}
function ConfigItem({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div>
<p className="text-sm font-medium mb-0.5">{label}</p>
<p className={clsx("text-sm text-muted-foreground break-all", mono && "font-mono")}>{value}</p>
<p className={cn("text-sm text-muted-foreground break-all", mono && "font-mono")}>{value}</p>
</div>
)
}
function EnvVarItem({ name, description, example }: { name: string; description: string; example: string }) {
return (
<div className="bg-muted/50 rounded-md px-3 py-2 grid gap-1.5">
<div className="bg-muted/50 rounded-md px-3 py-2.5 grid gap-1.5">
<code className="text-sm font-mono text-primary font-medium leading-tight">{name}</code>
<p className="text-sm text-muted-foreground">{description}</p>
<p className="text-xs text-muted-foreground">

View File

@@ -90,13 +90,6 @@ export default function SettingsLayout() {
icon: AlertOctagonIcon,
preload: alertsHistoryDataTableSettingsImport,
},
{
title: t`YAML Config`,
href: getPagePath($router, "settings", { name: "config" }),
icon: FileSlidersIcon,
admin: true,
preload: configYamlSettingsImport,
},
{
title: t`Heartbeat`,
href: getPagePath($router, "settings", { name: "heartbeat" }),
@@ -104,6 +97,13 @@ export default function SettingsLayout() {
admin: true,
preload: heartbeatSettingsImport,
},
{
title: t`YAML Config`,
href: getPagePath($router, "settings", { name: "config" }),
icon: FileSlidersIcon,
admin: true,
preload: configYamlSettingsImport,
},
]
const page = useStore($router)

View File

@@ -25,6 +25,7 @@ import {
$containerFilter,
$direction,
$maxValues,
$pveFilter,
$systems,
$temperatureFilter,
$userSettings,
@@ -160,6 +161,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
const [pveData, setPveData] = useState([] as ChartData["containerData"])
const temperatureChartRef = useRef<HTMLDivElement>(null)
const persistChartTime = useRef(false)
const [bottomSpacing, setBottomSpacing] = useState(0)
@@ -177,8 +179,10 @@ export default memo(function SystemDetail({ id }: { id: string }) {
persistChartTime.current = false
setSystemStats([])
setContainerData([])
setPveData([])
setDetails({} as SystemDetailsRecord)
$containerFilter.set("")
$pveFilter.set("")
}
}, [id])
@@ -277,6 +281,10 @@ export default memo(function SystemDetail({ id }: { id: string }) {
// Share chart config computation for all container charts
const containerChartConfigs = useContainerChartConfigs(containerData)
// PVE chart data and configs
const pveSyntheticChartData = useMemo(() => ({ ...chartData, containerData: pveData }), [chartData, pveData])
const pveChartConfigs = useContainerChartConfigs(pveData)
// make container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
const containerData = [] as ChartData["containerData"]
@@ -307,7 +315,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
Promise.allSettled([
getStats<SystemStatsRecord>("system_stats", system, chartTime),
getStats<ContainerStatsRecord>("container_stats", system, chartTime),
]).then(([systemStats, containerStats]) => {
getStats<ContainerStatsRecord>("pve_stats", system, chartTime),
]).then(([systemStats, containerStats, pveStats]) => {
// loading: false
setChartLoading(false)
@@ -334,6 +343,17 @@ export default memo(function SystemDetail({ id }: { id: string }) {
cache.set(cs_cache_key, containerData)
}
setContainerData(makeContainerData(containerData))
// make new pve stats
const ps_cache_key = `${system.id}_${chartTime}_pve_stats`
let pveRecords = (cache.get(ps_cache_key) || []) as ContainerStatsRecord[]
if (pveStats.status === "fulfilled" && pveStats.value.length) {
pveRecords = pveRecords.concat(addEmptyValues(pveRecords, pveStats.value, expectedInterval))
if (pveRecords.length > 120) {
pveRecords = pveRecords.slice(-100)
}
cache.set(ps_cache_key, pveRecords)
}
setPveData(makeContainerData(pveRecords))
})
}, [system, chartTime])
@@ -399,6 +419,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const showMax = maxValues && isLongerChart
const containerFilterBar = containerData.length ? <FilterBar /> : null
const pveFilterBar = pveData.length ? <FilterBar store={$pveFilter} /> : null
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
const lastGpus = systemStats.at(-1)?.stats?.g
@@ -493,6 +514,24 @@ export default memo(function SystemDetail({ id }: { id: string }) {
</ChartCard>
)}
{pveFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Proxmox CPU Usage`}
description={t`Average CPU utilization of VMs and containers`}
cornerEl={pveFilterBar}
>
<ContainerChart
chartData={pveSyntheticChartData}
dataKey="c"
chartType={ChartType.CPU}
chartConfig={pveChartConfigs.cpu}
filterStore={$pveFilter}
/>
</ChartCard>
)}
<ChartCard
empty={dataEmpty}
grid={grid}
@@ -520,6 +559,24 @@ export default memo(function SystemDetail({ id }: { id: string }) {
</ChartCard>
)}
{pveFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Proxmox Memory Usage`}
description={t`Memory usage of Proxmox VMs and containers`}
cornerEl={pveFilterBar}
>
<ContainerChart
chartData={pveSyntheticChartData}
dataKey="m"
chartType={ChartType.Memory}
chartConfig={pveChartConfigs.memory}
filterStore={$pveFilter}
/>
</ChartCard>
)}
<ChartCard empty={dataEmpty} grid={grid} title={t`Disk Usage`} description={t`Usage of root partition`}>
<DiskChart chartData={chartData} dataKey="stats.du" diskSize={systemStats.at(-1)?.stats.d ?? NaN} />
</ChartCard>
@@ -641,6 +698,24 @@ export default memo(function SystemDetail({ id }: { id: string }) {
</ChartCard>
)}
{pveFilterBar && pveData.length > 0 && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Proxmox Network I/O`}
description={t`Network traffic of Proxmox VMs and containers`}
cornerEl={pveFilterBar}
>
<ContainerChart
chartData={pveSyntheticChartData}
chartType={ChartType.Network}
dataKey="n"
chartConfig={pveChartConfigs.network}
filterStore={$pveFilter}
/>
</ChartCard>
)}
{/* Swap chart */}
{(systemStats.at(-1)?.stats.su ?? 0) > 0 && (
<ChartCard
@@ -877,6 +952,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<LazyContainersTable systemId={system.id} />
)}
{pveData.length > 0 && <LazyPveTable systemId={system.id} />}
{isLinux && compareSemVer(chartData.agentVersion, parseSemVer("0.16.0")) >= 0 && (
<LazySystemdTable systemId={system.id} />
)}
@@ -1051,6 +1128,17 @@ function LazyContainersTable({ systemId }: { systemId: string }) {
)
}
const PveTable = lazy(() => import("../pve-table/pve-table"))
function LazyPveTable({ systemId }: { systemId: string }) {
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
return (
<div ref={ref} className={cn(isIntersecting && "contents")}>
{isIntersecting && <PveTable systemId={systemId} />}
</div>
)
}
const SmartTable = lazy(() => import("./system/smart-table"))
function LazySmartTable({ systemId }: { systemId: string }) {

View File

@@ -621,8 +621,8 @@ function DiskSheet({
const deviceName = disk?.name || unknown
const model = disk?.model || unknown
const capacity = disk?.capacity ? formatCapacity(disk.capacity) : unknown
const serialNumber = disk?.serial || unknown
const firmwareVersion = disk?.firmware || unknown
const serialNumber = disk?.serial
const firmwareVersion = disk?.firmware
const status = disk?.state || unknown
return (
@@ -636,34 +636,42 @@ function DiskSheet({
{model}
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
{capacity}
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<Tooltip>
<TooltipTrigger asChild>
<span>{serialNumber}</span>
</TooltipTrigger>
<TooltipContent>
<Trans>Serial Number</Trans>
</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<Tooltip>
<TooltipTrigger asChild>
<span>{firmwareVersion}</span>
</TooltipTrigger>
<TooltipContent>
<Trans>Firmware</Trans>
</TooltipContent>
</Tooltip>
{serialNumber && (
<>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<Tooltip>
<TooltipTrigger asChild>
<span>{serialNumber}</span>
</TooltipTrigger>
<TooltipContent>
<Trans>Serial Number</Trans>
</TooltipContent>
</Tooltip>
</>
)}
{firmwareVersion && (
<>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<Tooltip>
<TooltipTrigger asChild>
<span>{firmwareVersion}</span>
</TooltipTrigger>
<TooltipContent>
<Trans>Firmware</Trans>
</TooltipContent>
</Tooltip>
</>
)}
</SheetDescription>
</SheetHeader>
<div className="flex-1 overflow-auto p-4 flex flex-col gap-4">
<div className="flex-1 overflow-hidden p-4 flex flex-col gap-4">
{isLoading ? (
<div className="flex justify-center py-8">
<LoaderCircleIcon className="animate-spin size-10 opacity-60" />
</div>
) : (
<>
<Alert className="pb-3">
<Alert className="pb-3 shrink-0">
{status === "PASSED" ? <CheckCircle2Icon className="size-4" /> : <XCircleIcon className="size-4" />}
<AlertTitle>
<Trans>S.M.A.R.T. Self-Test</Trans>: {status}
@@ -675,9 +683,9 @@ function DiskSheet({
)}
</Alert>
{smartAttributes.length > 0 ? (
<div className="rounded-md border overflow-auto">
<div className="rounded-md border min-h-0 flex flex-col">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (

View File

@@ -33,7 +33,6 @@ import {
decimalString,
formatBytes,
formatTemperature,
getMeterState,
parseSemVer,
secondsToUptimeString,
} from "@/lib/utils"
@@ -81,6 +80,10 @@ const STATUS_COLORS = {
[SystemStatus.Pending]: "bg-yellow-500",
} as const
function getMeterStateByThresholds(value: number, warn = 65, crit = 90): MeterState {
return value >= crit ? MeterState.Crit : value >= warn ? MeterState.Warn : MeterState.Good
}
/**
* @param viewMode - "table" or "grid"
* @returns - Column definitions for the systems table
@@ -209,6 +212,7 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
header: sortableHeader,
cell(info: CellContext<SystemRecord, unknown>) {
const { info: sysInfo, status } = info.row.original
const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] })
// agent version
const { minor, patch } = parseSemVer(sysInfo.v)
let loadAverages = sysInfo.la
@@ -224,7 +228,7 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
}
const normalizedLoad = max / (sysInfo.t ?? 1)
const threshold = getMeterState(normalizedLoad * 100)
const threshold = getMeterStateByThresholds(normalizedLoad * 100, colorWarn, colorCrit)
return (
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
@@ -463,8 +467,9 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
}
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] })
const val = Number(info.getValue()) || 0
const threshold = getMeterState(val)
const threshold = getMeterStateByThresholds(val, colorWarn, colorCrit)
const meterClass = cn(
"h-full",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
@@ -483,6 +488,7 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
}
function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] })
const { info: sysInfo, status, id } = info.row.original
const extraFs = Object.entries(sysInfo.efs ?? {})
@@ -496,7 +502,7 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
extraFs.sort((a, b) => b[1] - a[1])
function getIndicatorColor(pct: number) {
const threshold = getMeterState(pct)
const threshold = getMeterStateByThresholds(pct, colorWarn, colorCrit)
return (
(status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
@@ -514,7 +520,9 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
const extraDiskIndicators =
status !== SystemStatus.Up
? []
: [...new Set(extraFs.map(([, pct]) => getMeterState(pct)))].sort().map((state) => stateColors[state])
: [...new Set(extraFs.map(([, pct]) => getMeterStateByThresholds(pct, colorWarn, colorCrit)))]
.sort()
.map((state) => stateColors[state])
return (
<Tooltip>

View File

@@ -185,3 +185,12 @@ export function PlugChargingIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
// simple-icons (CC0) https://github.com/simple-icons/simple-icons/blob/develop/LICENSE.md
export function ProxmoxIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props} stroke="currentColor" strokeWidth="1.5" fill="none">
<path d="M5 1.8c-1.2.6-1.2.7-.1 1.8l7 7.8c.2 0 8-8.6 8.1-8.8l-.5-.5q-.5-.5-1.7-.5c-1.6-.1-2.2.2-4.1 2.4L12 6 10.4 4 8 1.9c-.8-.4-2.4-.5-3.2 0M1.2 4.4q-1.2.5-1.3.8l3 3.5L5.8 12l-3 3.3L0 18.8c.1.5 1.5 1 2.6 1 1.7 0 2-.2 5.6-4.1l3.2-3.7a74 74 0 0 0-7.1-7.5c-.9-.4-2.2-.5-3-.1m18.5 0q-.7.4-4 4L12.6 12l3.3 3.7c3.5 3.9 3.9 4.2 5.6 4.2 1 0 2.4-.6 2.5-1 0-.2-1.3-1.8-2.9-3.6L18 12l3-3.3c1.6-1.8 3-3.3 2.9-3.5 0-.4-1.4-1-2.5-1q-1 0-1.7.3M8 17l-4 4.4.5.6q.6.4 1.7.4c1.6.1 2.2-.2 4.2-2.5l1.6-1.8 1.7 1.8c2 2.3 2.5 2.6 4 2.5q1.3 0 1.8-.4t.5-.6c0-.2-7.9-8.8-8-8.8z" />
</svg>
)
}

View File

@@ -40,7 +40,7 @@ export const alertInfo: Record<string, AlertInfo> = {
unit: " MB/s",
icon: EthernetIcon,
desc: () => t`Triggers when combined up/down exceeds a threshold`,
max: 125,
max: 250,
},
GPU: {
name: () => t`GPU Usage`,

View File

@@ -8,7 +8,7 @@ export default [
["es", "Español", "🇪🇸"],
["fa", "فارسی", "🇮🇷"],
["fr", "Français", "🇫🇷"],
["he", "עברית", "🕎"],
["he", "עברית", ""],
["hr", "Hrvatski", "🇭🇷"],
["hu", "Magyar", "🇭🇺"],
["id", "Indonesia", "🇮🇩"],

View File

@@ -55,6 +55,9 @@ listenKeys($userSettings, ["chartTime"], ({ chartTime }) => $chartTime.set(chart
/** Container chart filter */
export const $containerFilter = atom("")
/** PVE chart filter */
export const $pveFilter = atom("")
/** Temperature chart filter */
export const $temperatureFilter = atom("")

View File

@@ -6,7 +6,7 @@ import { useEffect, useState } from "react"
import { twMerge } from "tailwind-merge"
import { toast } from "@/components/ui/use-toast"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { HourFormat, MeterState, Unit } from "./enums"
import { HourFormat, Unit } from "./enums"
import { $copyContent, $userSettings } from "./stores"
export function cn(...inputs: ClassValue[]) {
@@ -210,7 +210,6 @@ export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterf
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue, storageInterface)
})
// biome-ignore lint/correctness/useExhaustiveDependencies: storageInterface won't change
useEffect(() => {
storageInterface?.setItem(key, JSON.stringify(value))
}, [key, value])
@@ -394,12 +393,6 @@ export function compareSemVer(a: SemVer, b: SemVer) {
return a.patch - b.patch
}
/** Get meter state from 0-100 value. Used for color coding meters. */
export function getMeterState(value: number): MeterState {
const { colorWarn = 65, colorCrit = 90 } = $userSettings.get()
return value >= colorCrit ? MeterState.Crit : value >= colorWarn ? MeterState.Warn : MeterState.Good
}
// biome-ignore lint/suspicious/noExplicitAny: any is used to allow any function to be passed in
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout>

View File

@@ -93,6 +93,7 @@ msgstr "إجراءات"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "نشط"
@@ -140,6 +141,10 @@ msgstr "مسؤول"
msgid "After"
msgstr "بعد"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "بعد تعيين متغيرات البيئة، أعد تشغيل مركز Beszel الخاص بك لتصبح التغييرات سارية المفعول."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "وكيل"
@@ -350,6 +355,10 @@ msgstr "تحقق من {email} للحصول على رابط إعادة التعي
msgid "Check logs for more details."
msgstr "تحقق من السجلات لمزيد من التفاصيل."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "تحقق من خدمة المراقبة الخاصة بك"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "تحقق من خدمة الإشعارات الخاصة بك"
@@ -643,6 +652,14 @@ msgstr "فارغة"
msgid "End Time"
msgstr "وقت النهاية"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "عنوان URL للنقطة النهائية"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "عنوان URL للنقطة النهائية لل ping (مطلوب)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيين كلمة المرور"
@@ -662,6 +679,9 @@ msgstr "مؤقت"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "مؤقت"
msgid "Error"
msgstr "خطأ"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "مثال:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "فشل في المصادقة"
msgid "Failed to save settings"
msgstr "فشل في حفظ الإعدادات"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "فشل في إرسال نبضة القلب"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "فشل في إرسال إشعار الاختبار"
@@ -806,6 +834,18 @@ msgstr "شبكة"
msgid "Health"
msgstr "الصحة"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "نبضة القلب"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "مراقبة نبضة القلب"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "تم إرسال نبضة القلب بنجاح"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "أمر Homebrew"
msgid "Host / IP"
msgstr "مضيف / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "طريقة HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "طريقة HTTP: POST، GET، أو HEAD (الافتراضي: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "صورة"
msgid "Inactive"
msgstr "غير نشط"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "الفاصل الزمني"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "عنوان البريد الإشباكي غير صالح."
@@ -1110,6 +1162,10 @@ msgstr "متوقف مؤقتا"
msgid "Paused ({pausedSystemsLength})"
msgstr "متوقف مؤقتا ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "تنسيق الحمولة"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "بحث"
msgid "Search for systems or settings..."
msgstr "البحث عن الأنظمة أو الإعدادات..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "ثواني بين ال pings (الافتراضي: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "راجع <0>إعدادات الإشعارات</0> لتكوين كيفية تلقي التنبيهات."
@@ -1315,6 +1375,18 @@ msgstr "راجع <0>إعدادات الإشعارات</0> لتكوين كيفي
msgid "Select {foo}"
msgstr "تحديد {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "أرسل ping نبضة قلب واحدة للتحقق من أن نقطة النهاية الخاصة بك تعمل."
#: 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 "أرسل pings صادرة دورية إلى خدمة مراقبة خارجية حتى تتمكن من مراقبة Beszel دون تعريضه للإنترنت."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "إرسال نبضة قلب اختبارية"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "تم الإرسال"
@@ -1335,6 +1407,10 @@ msgstr "الخدمات"
msgid "Set percentage thresholds for meter colors."
msgstr "تعيين عتبات النسبة المئوية لألوان العداد."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "قم بتعيين متغيرات البيئة التالية على مركز Beszel الخاص بك لتمكين مراقبة نبضة القلب:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "درجات حرارة مستشعرات النظام"
msgid "Test <0>URL</0>"
msgstr "اختبار <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "اختبار نبضة القلب"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
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>موافق</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."
msgstr "ثم قم بتسجيل الدخول إلى الواجهة الخلفية وأعد تعيين كلمة مرور حساب المستخدم الخاص بك في جدول المستخدمين."
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "رفع"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "مدة التشغيل"
@@ -1716,6 +1801,10 @@ msgstr "إشعارات Webhook / Push"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "عند التفعيل، يسمح هذا الرمز المميز للوكلاء بالتسجيل الذاتي دون إنشاء نظام مسبق."
#: 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، تتضمن كل نبضة قلب حمولة JSON مع ملخص حالة النظام وقائمة الأنظمة المعطلة والتنبيهات التي تم تشغيلها."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Действия"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Активен"
@@ -140,6 +141,10 @@ msgstr "Администратор"
msgid "After"
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, за да влязат промените в сила."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Агент"
@@ -231,7 +236,7 @@ msgstr "Bandwidth на мрежата"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Бат"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Провери {email} за линк за нулиране."
msgid "Check logs for more details."
msgstr "Провери log-овете за повече информация."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Проверете вашата услуга за мониторинг"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Провери услугата си за удостоверяване"
@@ -621,7 +630,7 @@ msgstr "Редактирай"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr ""
msgstr "Редактиране на {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -643,6 +652,14 @@ msgstr "Празна"
msgid "End Time"
msgstr "Крайно време"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL адрес на крайната точка"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL адрес на крайната точка за пинг (задължително)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Въведи имейл адрес за да нулираш паролата"
@@ -662,6 +679,9 @@ msgstr "Ефимерен"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Ефимерен"
msgid "Error"
msgstr "Грешка"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Пример:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -679,7 +703,7 @@ msgstr "Надвишава {0}{1} в последните {2, plural, one {# м
#: src/components/systemd-table/systemd-table.tsx
msgid "Exec main PID"
msgstr ""
msgstr "PID на главния изпълнителен процес"
#: src/components/routes/settings/config-yaml.tsx
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
@@ -727,6 +751,10 @@ msgstr "Неуспешно удостоверяване"
msgid "Failed to save settings"
msgstr "Неуспешно запазване на настройки"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Неуспешно изпращане на heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Неуспешно изпрати тестова нотификация"
@@ -806,6 +834,18 @@ msgstr "Мрежово"
msgid "Health"
msgstr "Здраве"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Мониторинг на heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat е изпратен успешно"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Команда Homebrew"
msgid "Host / IP"
msgstr "Хост / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP метод"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP метод: POST, GET или HEAD (по подразбиране: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Образ"
msgid "Inactive"
msgstr "Неактивен"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Интервал"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Невалиден имейл адрес."
@@ -914,7 +966,7 @@ msgstr "Търсиш къде да създадеш тревоги? Натисн
#: src/components/systemd-table/systemd-table.tsx
msgid "Main PID"
msgstr ""
msgstr "Главен PID"
#: src/components/routes/settings/layout.tsx
msgid "Manage display and notification preferences."
@@ -1110,6 +1162,10 @@ msgstr "На пауза"
msgid "Paused ({pausedSystemsLength})"
msgstr "На пауза ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Формат на полезния товар"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "Търси"
msgid "Search for systems or settings..."
msgstr "Търси за системи или настройки..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Секунди между пинговете (по подразбиране: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Виж <0>настройките за нотификациите</0> за да конфигурираш как получаваш тревоги."
@@ -1315,6 +1375,18 @@ msgstr "Виж <0>настройките за нотификациите</0> з
msgid "Select {foo}"
msgstr "Избери {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Изпратете единичен heartbeat пинг, за да проверите дали вашата крайна точка работи."
#: 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 "Изпращайте периодични изходящи пингове към външна услуга за мониторинг, за да можете да наблюдавате Beszel, без да го излагате на интернет."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Изпращане на тестов heartbeat"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Изпратени"
@@ -1335,6 +1407,10 @@ msgstr "Услуги"
msgid "Set percentage thresholds for meter colors."
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:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Температири на системни сензори"
msgid "Test <0>URL</0>"
msgstr "Тествай <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Тестов heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
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>warn</1>, когато са задействани предупреждения, и <2>error</2>, когато някоя система е спряла."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "След това влез в backend-а и нулирай паролата за потребителския акаунт в таблицата за потребители."
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "Качване"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Време на работа"
msgstr "Uptime"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Пуш нотификации"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Когато е активиран, този символ позволява на агентите да се регистрират сами без предварително създаване на система."
#: 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 полезен товар с резюме на състоянието на системата, списък на спрените системи и задействаните предупреждения."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Akce"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktivní"
@@ -140,6 +141,10 @@ msgstr "Administrátor"
msgid "After"
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 nastavení proměnných prostředí restartujte hub Beszel, aby se změny projevily."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
@@ -231,7 +236,7 @@ msgstr "Přenos"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Bat"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Zkontrolujte {email} pro odkaz na obnovení."
msgid "Check logs for more details."
msgstr "Pro více informací zkontrolujte logy."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Zkontrolujte svou monitorovací službu"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Zkontrolujte službu upozornění"
@@ -556,11 +565,11 @@ msgstr "Vybíjení"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Disk"
msgstr ""
msgstr "Disk"
#: src/components/routes/system.tsx
msgid "Disk I/O"
msgstr ""
msgstr "Disk I/O"
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
@@ -643,6 +652,14 @@ msgstr "Prázdná"
msgid "End Time"
msgstr "Čas ukončení"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL koncového bodu"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL koncového bodu pro ping (vyžadováno)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Zadejte e-mailovou adresu pro obnovu hesla"
@@ -662,6 +679,9 @@ msgstr "Efemérní"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Efemérní"
msgid "Error"
msgstr "Chyba"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Příklad:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Ověření se nezdařilo"
msgid "Failed to save settings"
msgstr "Nepodařilo se uložit nastavení"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Nepodařilo se odeslat heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Nepodařilo se odeslat testovací oznámení"
@@ -755,7 +783,7 @@ msgstr "Otisk"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr ""
msgstr "Firmware"
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -806,6 +834,18 @@ msgstr "Mřížka"
msgid "Health"
msgstr "Zdraví"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Monitorování heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat úspěšně odeslán"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew příkaz"
msgid "Host / IP"
msgstr "Hostitel / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP metoda"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metoda: POST, GET nebo HEAD (výchozí: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Obraz"
msgid "Inactive"
msgstr "Neaktivní"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Interval"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Neplatná e-mailová adresa."
@@ -858,7 +910,7 @@ msgstr "Životní cyklus"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "limit"
msgstr ""
msgstr "limit"
#: src/components/routes/system.tsx
msgid "Load Average"
@@ -958,7 +1010,7 @@ msgstr "Využití paměti docker kontejnerů"
#: src/components/routes/system/smart-table.tsx
msgid "Model"
msgstr ""
msgstr "Model"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
@@ -1110,6 +1162,10 @@ msgstr "Pozastaveno"
msgid "Paused ({pausedSystemsLength})"
msgstr "Pozastaveno ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Formát payloadu"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1162,7 +1218,7 @@ msgstr "Přihlaste se prosím k vašemu účtu"
#: src/components/add-system.tsx
msgid "Port"
msgstr ""
msgstr "Port"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1307,6 +1363,10 @@ msgstr "Hledat"
msgid "Search for systems or settings..."
msgstr "Hledat systémy nebo nastavení..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Sekundy mezi pingy (výchozí: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak přijímáte upozornění."
@@ -1315,6 +1375,18 @@ msgstr "Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak
msgid "Select {foo}"
msgstr "Vybrat {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Odešlete jeden heartbeat ping pro ověření funkčnosti vašeho koncového bodu."
#: 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 "Odesílejte periodické odchozí pingy na externí monitorovací službu, abyste mohli monitorovat Beszel bez jeho vystavení internetu."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Odeslat testovací heartbeat"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Odeslat"
@@ -1335,6 +1407,10 @@ msgstr "Služby"
msgid "Set percentage thresholds for meter colors."
msgstr "Nastavte procentuální prahové hodnoty pro barvy měřičů."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Pro povolení monitorování heartbeat nastavte na hubu Beszel následující proměnné prostředí:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Teploty systémových senzorů"
msgid "Test <0>URL</0>"
msgstr "Testovat <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Testovat heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Testovací oznámení odesláno"
#: 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 "Celkový stav je <0>ok</0>, když jsou všechny systémy v provozu, <1>warn</1>, když jsou spuštěny výstrahy, a <2>error</2>, když je některý systém mimo provoz."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Poté se přihlaste do backendu a obnovte heslo k uživatelskému účtu v tabulce uživatelů."
@@ -1497,7 +1581,7 @@ msgstr "Přepnout motiv"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr ""
msgstr "Token"
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "Odeslání"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Doba provozu"
msgstr "Uptime"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Push oznámení"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Pokud je povoleno, umožňuje tento token agentům samo-registraci bez předchozího vytvoření systému."
#: 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 "Při použití metody POST obsahuje každý heartbeat JSON payload se souhrnem stavu systému, seznamem nefunkčních systémů a spuštěnými výstrahami."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Handlinger"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktiv"
@@ -140,6 +141,10 @@ msgstr "Administrator"
msgid "After"
msgstr "Efter"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Efter indstilling af miljøvariablerne skal du genstarte din Beszel-hub for at ændringerne kan træde i kraft."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
@@ -231,7 +236,7 @@ msgstr "Båndbredde"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Bat"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Tjek {email} for et nulstillingslink."
msgid "Check logs for more details."
msgstr "Tjek logfiler for flere detaljer."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Tjek din overvågningstjeneste"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Tjek din notifikationstjeneste"
@@ -643,6 +652,14 @@ msgstr "Tom"
msgid "End Time"
msgstr "Sluttid"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "Endpoint-URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "Endpoint-URL til ping (påkrævet)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Indtast emailadresse for at nulstille adgangskoden"
@@ -662,6 +679,9 @@ msgstr "Efemer"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Efemer"
msgid "Error"
msgstr "Fejl"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Eksempel:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Kunne ikke godkende"
msgid "Failed to save settings"
msgstr "Kunne ikke gemme indstillinger"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Kunne ikke sende heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Afsendelse af testnotifikation mislykkedes"
@@ -806,6 +834,18 @@ msgstr "Gitter"
msgid "Health"
msgstr "Sundhed"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Heartbeat-overvågning"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat sendt succesfuldt"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew-kommando"
msgid "Host / IP"
msgstr "Vært / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP-metode"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP-metode: POST, GET eller HEAD (standard: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Billede"
msgid "Inactive"
msgstr "Inaktiv"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Interval"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Ugyldig email adresse."
@@ -1110,6 +1162,10 @@ msgstr "Sat på pause"
msgid "Paused ({pausedSystemsLength})"
msgstr "Sat på pause ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Payload-format"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1121,7 +1177,7 @@ msgstr "Procentdel af tid brugt i hver tilstand"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent"
msgstr ""
msgstr "Permanent"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence"
@@ -1307,6 +1363,10 @@ msgstr "Søg"
msgid "Search for systems or settings..."
msgstr "Søg efter systemer eller indstillinger..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Sekunder mellem pings (standard: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtager alarmer."
@@ -1315,6 +1375,18 @@ msgstr "Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtag
msgid "Select {foo}"
msgstr "Vælg {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Send et enkelt heartbeat-ping for at bekræfte, at dit endpoint fungerer."
#: 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 "Send periodiske udgående pings til en ekstern overvågningstjeneste, så du kan overvåge Beszel uden at eksponere det for internettet."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Send test-heartbeat"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Sendt"
@@ -1335,6 +1407,10 @@ msgstr "Tjenester"
msgid "Set percentage thresholds for meter colors."
msgstr "Indstil procentvise tærskler for målerfarver."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Indstil følgende miljøvariabler på din Beszel-hub for at aktivere heartbeat-overvågning:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Temperaturer i systemsensorer"
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Test-heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Test notifikation sendt"
#: 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 "Den overordnede status er <0>ok</0>, når alle systemer kører, <1>warn</1>, når alarmer udløses, og <2>error</2>, når et system er nede."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Log derefter ind på backend og nulstil adgangskoden til din brugerkonto i tabellen brugere."
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "Overfør"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Oppetid"
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Push notifikationer"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Når aktiveret, tillader denne token agenter at registrere sig selv uden forudgående systemoprettelse."
#: 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 "Når du bruger POST, inkluderer hvert heartbeat en JSON-payload med resumé af systemstatus, liste over systemer, der er nede, og udløste alarmer."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Aktionen"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktiv"
@@ -140,6 +141,10 @@ msgstr "Admin"
msgid "After"
msgstr "Nach"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Starten Sie nach dem Festlegen der Umgebungsvariablen Ihren Beszel-Hub neu, damit die Änderungen wirksam werden."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
@@ -231,7 +236,7 @@ msgstr "Bandbreite"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Bat"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Überprüfe {email} auf einen Link zum Zurücksetzen."
msgid "Check logs for more details."
msgstr "Überprüfe die Protokolle für weitere Details."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Überprüfen Sie Ihren Überwachungsdienst"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Überprüfe deinen Benachrichtigungsdienst"
@@ -643,6 +652,14 @@ msgstr "Leer"
msgid "End Time"
msgstr "Endzeit"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "Endpunkt-URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "Endpunkt-URL zum Pingen (erforderlich)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen"
@@ -662,6 +679,9 @@ msgstr "Flüchtig"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Flüchtig"
msgid "Error"
msgstr "Fehler"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Beispiel:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Authentifizierung fehlgeschlagen"
msgid "Failed to save settings"
msgstr "Einstellungen konnten nicht gespeichert werden"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Heartbeat konnte nicht gesendet werden"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Testbenachrichtigung konnte nicht gesendet werden"
@@ -806,6 +834,18 @@ msgstr "Raster"
msgid "Health"
msgstr "Gesundheit"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Heartbeat-Überwachung"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat erfolgreich gesendet"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew-Befehl"
msgid "Host / IP"
msgstr "Host / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP-Methode"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP-Methode: POST, GET oder HEAD (Standard: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Image"
msgid "Inactive"
msgstr "Inaktiv"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Intervall"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Ungültige E-Mail-Adresse."
@@ -1096,7 +1148,7 @@ msgstr "Anfrage zum Zurücksetzen des Passworts erhalten"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr ""
msgstr "Vergangen"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
@@ -1110,6 +1162,10 @@ msgstr "Pausiert"
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausiert ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Payload-Format"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1121,7 +1177,7 @@ msgstr "Prozentsatz der Zeit in jedem Zustand"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent"
msgstr ""
msgstr "Permanent"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence"
@@ -1248,7 +1304,7 @@ msgstr "Fortsetzen"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr ""
msgstr "Root"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1307,6 +1363,10 @@ msgstr "Suche"
msgid "Search for systems or settings..."
msgstr "Nach Systemen oder Einstellungen suchen..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Sekunden zwischen Pings (Standard: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du Warnungen erhältst."
@@ -1315,6 +1375,18 @@ msgstr "Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du
msgid "Select {foo}"
msgstr "Auswählen {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Senden Sie einen einzelnen Heartbeat-Ping, um zu überprüfen, ob Ihr Endpunkt funktioniert."
#: 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 "Senden Sie regelmäßige ausgehende Pings an einen externen Überwachungsdienst, damit Sie Beszel überwachen können, ohne es dem Internet auszusetzen."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Test-Heartbeat senden"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Gesendet"
@@ -1335,6 +1407,10 @@ msgstr "Dienste"
msgid "Set percentage thresholds for meter colors."
msgstr "Prozentuale Schwellenwerte für Zählerfarben festlegen."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Legen Sie die folgenden Umgebungsvariablen auf Ihrem Beszel-Hub fest, um die Heartbeat-Überwachung zu aktivieren:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Temperaturen der Systemsensoren"
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Test-Heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Testbenachrichtigung gesendet"
#: 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 "Der Gesamtstatus ist <0>ok</0>, wenn alle Systeme in Betrieb sind, <1>warn</1>, wenn Warnungen ausgelöst werden, und <2>error</2>, wenn ein System ausgefallen ist."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Melde dich dann im Backend an und setze dein Benutzerkontopasswort in der Benutzertabelle zurück."
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "Hochladen"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Betriebszeit"
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Push-Benachrichtigungen"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Wenn aktiviert, ermöglicht dieser Token Agenten die Selbstregistrierung ohne vorherige Systemerstellung."
#: 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 "Bei Verwendung von POST enthält jeder Heartbeat eine JSON-Payload mit einer Zusammenfassung des Systemstatus, einer Liste der ausgefallenen Systeme und ausgelösten Warnungen."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -88,6 +88,7 @@ msgstr "Actions"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Active"
@@ -135,6 +136,10 @@ msgstr "Admin"
msgid "After"
msgstr "After"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "After setting the environment variables, restart your Beszel hub for changes to take effect."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
@@ -345,6 +350,10 @@ msgstr "Check {email} for a reset link."
msgid "Check logs for more details."
msgstr "Check logs for more details."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Check your monitoring service"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Check your notification service"
@@ -638,6 +647,14 @@ msgstr "Empty"
msgid "End Time"
msgstr "End Time"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "Endpoint URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "Endpoint URL to ping (required)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Enter email address to reset password"
@@ -657,6 +674,9 @@ msgstr "Ephemeral"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -665,6 +685,10 @@ msgstr "Ephemeral"
msgid "Error"
msgstr "Error"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Example:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -722,6 +746,10 @@ msgstr "Failed to authenticate"
msgid "Failed to save settings"
msgstr "Failed to save settings"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Failed to send heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Failed to send test notification"
@@ -801,6 +829,18 @@ msgstr "Grid"
msgid "Health"
msgstr "Health"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Heartbeat Monitoring"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat sent successfully"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -811,6 +851,14 @@ msgstr "Homebrew command"
msgid "Host / IP"
msgstr "Host / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP Method"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP method: POST, GET, or HEAD (default: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -829,6 +877,10 @@ msgstr "Image"
msgid "Inactive"
msgstr "Inactive"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Interval"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Invalid email address."
@@ -1105,6 +1157,10 @@ msgstr "Paused"
msgid "Paused ({pausedSystemsLength})"
msgstr "Paused ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Payload format"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1302,6 +1358,10 @@ msgstr "Search"
msgid "Search for systems or settings..."
msgstr "Search for systems or settings..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Seconds between pings (default: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "See <0>notification settings</0> to configure how you receive alerts."
@@ -1310,6 +1370,18 @@ msgstr "See <0>notification settings</0> to configure how you receive alerts."
msgid "Select {foo}"
msgstr "Select {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Send a single heartbeat ping to verify your endpoint is working."
#: 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 "Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Send test heartbeat"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Sent"
@@ -1330,6 +1402,10 @@ msgstr "Services"
msgid "Set percentage thresholds for meter colors."
msgstr "Set percentage thresholds for meter colors."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1447,10 +1523,18 @@ msgstr "Temperatures of system sensors"
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Test heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Test notification sent"
#: 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 "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."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Then log into the backend and reset your user account password in the users table."
@@ -1637,6 +1721,7 @@ msgid "Upload"
msgstr "Upload"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Uptime"
@@ -1711,6 +1796,10 @@ msgstr "Webhook / Push notifications"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "When enabled, this token allows agents to self-register without prior system creation."
#: 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 "When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Acciones"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Activo"
@@ -140,6 +141,10 @@ msgstr "Administrador"
msgid "After"
msgstr "Después"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Después de configurar las variables de entorno, reinicie su hub Beszel para que los cambios surtan efecto."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agente"
@@ -350,6 +355,10 @@ msgstr "Revisa {email} para un enlace de restablecimiento."
msgid "Check logs for more details."
msgstr "Revisa los registros para más detalles."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Compruebe su servicio de monitorización"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Verifica tu servicio de notificaciones"
@@ -643,6 +652,14 @@ msgstr "Vacía"
msgid "End Time"
msgstr "Hora de finalización"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL del punto de conexión"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL del punto de conexión para ping (obligatorio)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Ingresa la dirección de correo electrónico para restablecer la contraseña"
@@ -662,6 +679,9 @@ msgstr "Efímero"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Efímero"
msgid "Error"
msgstr "Error"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Ejemplo:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Error al autenticar"
msgid "Failed to save settings"
msgstr "Error al guardar la configuración"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Error al enviar el latido (heartbeat)"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Error al enviar la notificación de prueba"
@@ -806,6 +834,18 @@ msgstr "Cuadrícula"
msgid "Health"
msgstr "Estado"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Monitorización de Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Latido enviado con éxito"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Comando Homebrew"
msgid "Host / IP"
msgstr "Servidor / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "Método HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Método HTTP: POST, GET o HEAD (predeterminado: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Imagen"
msgid "Inactive"
msgstr "Inactivo"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Intervalo"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Dirección de correo electrónico no válida."
@@ -1110,6 +1162,10 @@ msgstr "Pausado"
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausado ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Formato de carga útil (payload)"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "Buscar"
msgid "Search for systems or settings..."
msgstr "Buscar sistemas o configuraciones..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Segundos entre pings (predeterminado: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Consulta la <0>configuración de notificaciones</0> para configurar cómo recibes alertas."
@@ -1315,6 +1375,18 @@ msgstr "Consulta la <0>configuración de notificaciones</0> para configurar cóm
msgid "Select {foo}"
msgstr "Seleccionar {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Envíe un único ping de latido para verificar que su punto de conexión funciona."
#: 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 "Envíe pings salientes periódicos a un servicio de monitorización externo para que pueda supervisar Beszel sin exponerlo a internet."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Enviar latido de prueba"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Enviado"
@@ -1335,6 +1407,10 @@ msgstr "Servicios"
msgid "Set percentage thresholds for meter colors."
msgstr "Establecer umbrales de porcentaje para los colores de los medidores."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Configure las siguientes variables de entorno en su hub Beszel para habilitar la monitorización de latidos:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Temperaturas de los sensores del sistema"
msgid "Test <0>URL</0>"
msgstr "Probar <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Probar latido"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Notificación de prueba enviada"
#: 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 "El estado general es <0>ok</0> cuando todos los sistemas están activos, <1>warn</1> cuando se activan alertas y <2>error</2> cuando algún sistema está caído."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Luego inicia sesión en el backend y restablece la contraseña de tu cuenta de usuario en la tabla de usuarios."
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "Cargar"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Tiempo de actividad"
msgstr "Uptime"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Notificaciones Webhook / Push"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Cuando está habilitado, este token permite a los agentes registrarse automáticamente sin creación previa del sistema."
#: 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 "Al usar POST, cada latido incluye una carga útil JSON con un resumen del estado del sistema, una lista de sistemas caídos y alertas activadas."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "عملیات"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "فعال"
@@ -140,6 +141,10 @@ msgstr "مدیر"
msgid "After"
msgstr "بعد از"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "پس از تنظیم متغیرهای محیطی، هاب Beszel خود را مجدداً راه اندازی کنید تا تغییرات اعمال شوند."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "عامل"
@@ -350,6 +355,10 @@ msgstr "ایمیل {email} خود را برای لینک بازنشانی برر
msgid "Check logs for more details."
msgstr "برای جزئیات بیشتر، لاگ‌ها را بررسی کنید."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "سرویس نظارتی خود را بررسی کنید"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "سرویس اطلاع‌رسانی خود را بررسی کنید"
@@ -643,6 +652,14 @@ msgstr "خالی"
msgid "End Time"
msgstr "زمان پایان"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL نقطه پایانی"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL نقطه پایانی برای پینگ (الزامی)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "آدرس ایمیل را برای بازنشانی رمز عبور وارد کنید"
@@ -662,6 +679,9 @@ msgstr "گذرا"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "گذرا"
msgid "Error"
msgstr "خطا"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "مثال:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "احراز هویت ناموفق بود"
msgid "Failed to save settings"
msgstr "ذخیره تنظیمات ناموفق بود"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "ارسال ضربان قلب ناموفق بود"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "ارسال اعلان آزمایشی ناموفق بود"
@@ -806,6 +834,18 @@ msgstr "جدول"
msgid "Health"
msgstr "سلامتی"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "ضربان قلب"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "نظارت بر ضربان قلب"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "ضربان قلب با موفقیت ارسال شد"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "دستور Homebrew"
msgid "Host / IP"
msgstr "میزبان / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "متد HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "متد HTTP: POST، GET، یا HEAD (پیش‌فرض: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "تصویر"
msgid "Inactive"
msgstr "غیرفعال"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "بازه زمانی"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "آدرس ایمیل نامعتبر است."
@@ -1110,6 +1162,10 @@ msgstr "مکث شده"
msgid "Paused ({pausedSystemsLength})"
msgstr "مکث شده ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "فرمت پی‌لود"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "جستجو"
msgid "Search for systems or settings..."
msgstr "جستجو برای سیستم‌ها یا تنظیمات..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "ثانیه بین پینگ‌ها (پیش‌فرض: ۶۰)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "برای پیکربندی نحوه دریافت هشدارها، به <0>تنظیمات اعلان</0> مراجعه کنید."
@@ -1315,6 +1375,18 @@ msgstr "برای پیکربندی نحوه دریافت هشدارها، به <0
msgid "Select {foo}"
msgstr "انتخاب {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "یک پینگ ضربان قلب تکی ارسال کنید تا از کارکرد نقطه پایانی خود اطمینان حاصل کنید."
#: 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 "پینگ‌های خروجی دوره‌ای را به یک سرویس نظارتی خارجی ارسال کنید تا بتوانید Beszel را بدون قرار دادن آن در معرض اینترنت نظارت کنید."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "ارسال ضربان قلب آزمایشی"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "ارسال شد"
@@ -1335,6 +1407,10 @@ msgstr "سرویس‌ها"
msgid "Set percentage thresholds for meter colors."
msgstr "آستانه های درصدی را برای رنگ های متر تنظیم کنید."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "متغیرهای محیطی زیر را در هاب Beszel خود تنظیم کنید تا نظارت بر ضربان قلب فعال شود:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "دمای حسگرهای سیستم"
msgid "Test <0>URL</0>"
msgstr "تست <0>آدرس اینترنتی</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "تست ضربان قلب"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
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>warn</1> زمانی که هشدارها فعال شوند، و <2>error</2> زمانی که هر سیستمی پایین باشد."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "سپس وارد بخش پشتیبان شوید و رمز عبور حساب کاربری خود را در جدول کاربران بازنشانی کنید."
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "آپلود"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "آپتایم"
@@ -1716,6 +1801,10 @@ msgstr "اعلان‌های Webhook / Push"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "هنگامی که فعال باشد، این توکن به عوامل اجازه می‌دهد بدون ایجاد سیستم قبلی، خود را ثبت کنند."
#: 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، هر ضربان قلب شامل یک پی‌لود JSON با خلاصه وضعیت سیستم، لیست سیستم‌های پایین و هشدارهای فعال شده است."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Actions"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Active"
@@ -140,6 +141,10 @@ msgstr "Admin"
msgid "After"
msgstr "Après"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Après avoir défini les variables d'environnement, redémarrez votre hub Beszel pour que les changements prennent effet."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
@@ -231,7 +236,7 @@ msgstr "Bande passante"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Bat"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Vérifiez {email} pour un lien de réinitialisation."
msgid "Check logs for more details."
msgstr "Vérifiez les journaux pour plus de détails."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Vérifiez votre service de surveillance"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Vérifiez votre service de notification"
@@ -643,6 +652,14 @@ msgstr "Vide"
msgid "End Time"
msgstr "Heure de fin"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL du point de terminaison"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL du point de terminaison à pinguer (requis)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Entrez l'adresse email pour réinitialiser le mot de passe"
@@ -662,6 +679,9 @@ msgstr "Éphémère"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Éphémère"
msgid "Error"
msgstr "Erreur"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Exemple :"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Échec de l'authentification"
msgid "Failed to save settings"
msgstr "Échec de l'enregistrement des paramètres"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Échec de l'envoi du battement de cœur (heartbeat)"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Échec de l'envoi de la notification de test"
@@ -806,6 +834,18 @@ msgstr "Grille"
msgid "Health"
msgstr "Santé"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Surveillance Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Battement de cœur envoyé avec succès"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Commande Homebrew"
msgid "Host / IP"
msgstr "Hôte / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "Méthode HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Méthode HTTP : POST, GET ou HEAD (par défaut : POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Image"
msgid "Inactive"
msgstr "Inactif"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Intervalle"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Adresse email invalide."
@@ -1110,6 +1162,10 @@ msgstr "En pause"
msgid "Paused ({pausedSystemsLength})"
msgstr "Mis en pause ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Format de la charge utile"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "Recherche"
msgid "Search for systems or settings..."
msgstr "Rechercher des systèmes ou des paramètres..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Secondes entre les pings (par défaut : 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Voir les <0>paramètres de notification</0> pour configurer comment vous recevez les alertes."
@@ -1315,6 +1375,18 @@ msgstr "Voir les <0>paramètres de notification</0> pour configurer comment vous
msgid "Select {foo}"
msgstr "Sélectionner {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Envoyez un seul ping heartbeat pour vérifier que votre point de terminaison fonctionne."
#: 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 "Envoyez des pings sortants périodiques vers un service de surveillance externe afin de pouvoir surveiller Beszel sans l'exposer à Internet."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Envoyer un heartbeat de test"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Envoyé"
@@ -1335,6 +1407,10 @@ msgstr "Services"
msgid "Set percentage thresholds for meter colors."
msgstr "Définir des seuils de pourcentage pour les couleurs des compteurs."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Définissez les variables d'environnement suivantes sur votre hub Beszel pour activer la surveillance du heartbeat :"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Températures des capteurs du système"
msgid "Test <0>URL</0>"
msgstr "Tester <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Tester le heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Notification de test envoyée"
#: 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 "L'état général est <0>ok</0> quand tous les systèmes sont opérationnels, <1>warn</1> quand des alertes sont déclenchées, et <2>error</2> quand un système est en panne."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Ensuite, connectez-vous au backend et réinitialisez le mot de passe de votre compte utilisateur dans la table des utilisateurs."
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "Téléverser"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Temps de fonctionnement"
msgstr "Uptime"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Notifications Webhook / Push"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Lorsqu'il est activé, ce jeton permet aux agents de s'enregistrer automatiquement sans création préalable du système."
#: 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 "En utilisant POST, chaque heartbeat inclut une charge utile JSON avec un résumé de l'état du sistema, la liste des systèmes en panne et les alertes déclenchées."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "פעולות"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "פעיל"
@@ -140,6 +141,10 @@ msgstr "מנהל"
msgid "After"
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 שלך כדי שהשינויים ייכנסו לתוקף."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "סוכן"
@@ -350,6 +355,10 @@ msgstr "בדוק את {email} לקישור איפוס."
msgid "Check logs for more details."
msgstr "בדוק לוגים לפרטים נוספים"
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "בדוק את שירות הניטור שלך"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "בדוק את שירות ההתראות שלך"
@@ -643,6 +652,14 @@ msgstr "ריק"
msgid "End Time"
msgstr "זמן סיום"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL של נקודת קצה"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL של נקודת קצה לפינג (חובה)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "הכנס כתובת אימייל לאיפוס סיסמה"
@@ -662,6 +679,9 @@ msgstr "זמני"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "זמני"
msgid "Error"
msgstr "שגיאה"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "דוגמה:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "אימות נכשל"
msgid "Failed to save settings"
msgstr "שמירת הגדרות נכשלה"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "שליחת פעימת הלב נכשלה"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "שליחת התראת בדיקה נכשלה"
@@ -806,6 +834,18 @@ msgstr "רשת"
msgid "Health"
msgstr "בריאות"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "פעימת לב"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "ניטור פעימות לב"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "פעימת הלב נשלחה בהצלחה"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "פקודת Homebrew"
msgid "Host / IP"
msgstr "מארח / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "שיטת HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "שיטת HTTP: POST, GET, או HEAD (ברירת מחדל: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "תמונה"
msgid "Inactive"
msgstr "לא פעיל"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "מרווח"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "כתובת אימייל לא תקינה."
@@ -1110,6 +1162,10 @@ msgstr "מושהה"
msgid "Paused ({pausedSystemsLength})"
msgstr "מושהה ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "פורמט מטען (Payload)"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "חיפוש"
msgid "Search for systems or settings..."
msgstr "חפש מערכות או הגדרות..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "שניות בין פינגים (ברירת מחדל: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "ראה <0>הגדרות התראות</0> כדי להגדיר כיצד אתה מקבל התראות."
@@ -1315,6 +1375,18 @@ msgstr "ראה <0>הגדרות התראות</0> כדי להגדיר כיצד א
msgid "Select {foo}"
msgstr "בחר {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "שלח פינג פעימת לב בודד כדי לוודא שנקודת הקצה שלך עובדת."
#: 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 "שלח פינגים יוצאים תקופתיים לשירות ניטור חיצוני כדי שתוכל לנטר את Beszel מבלי לחשוף אותו לאינטרנט."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "שלח פעימת לב לבדיקה"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "נשלח"
@@ -1335,6 +1407,10 @@ msgstr "שירותים"
msgid "Set percentage thresholds for meter colors."
msgstr "הגדר סף אחוזים עבור צבעי מד."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "הגדר את משתני הסביבה הבאים ב-Beszel hub שלך כדי לאפשר ניטור פעימות לב:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "טמפרטורות של חיישני המערכת"
msgid "Test <0>URL</0>"
msgstr "בדוק <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "בדוק פעימת לב"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
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>warn</1> כשמופעלות התראות, ו-<2>error</2> כשמערכת כלשהי מושבתת."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "לאחר מכן התחבר ל-backend ואפס את סיסמת חשבון המשתמש שלך בטבלת המשתמשים."
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "העלאה"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "זמן פעילות"
@@ -1716,6 +1801,10 @@ msgstr "Webhook / התראות דחיפה"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "כאשר מופעל, אסימון זה מאפשר לסוכנים להירשם באופן עצמי ללא יצירת מערכת מוקדמת."
#: 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, כל פעימת לב כוללת מטען JSON עם סיכום סטטוס המערכת, רשימת מערכות מושבתות והתראות שהופעלו."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Akcije"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktivan"
@@ -140,6 +141,10 @@ msgstr "Admin"
msgid "After"
msgstr "Nakon"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Nakon postavljanja varijabli okruženja, ponovno pokrenite svoj Beszel hub kako bi promjene stupile na snagu."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
@@ -231,7 +236,7 @@ msgstr "Mrežna Propusnost"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Bat"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Provjerite {email} za pristup poveznici za resetiranje."
msgid "Check logs for more details."
msgstr "Provjerite zapise (logove) za više detalja."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Provjerite svoju uslugu nadzora"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Provjerite svoju obavještajnu uslugu"
@@ -643,6 +652,14 @@ msgstr "Prazno"
msgid "End Time"
msgstr "Vrijeme završetka"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL krajnje točke"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL krajnje točke za pinganje (obavezno)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Unesite email adresu kako biste resetirali lozinku"
@@ -662,6 +679,9 @@ msgstr "Efemeran"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Efemeran"
msgid "Error"
msgstr "Greška"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Primjer:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Neuspješna provjera autentičnosti"
msgid "Failed to save settings"
msgstr "Neuspješno spremanje postavki"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Slanje heartbeata nije uspjelo"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Neuspješno slanje probne obavijesti"
@@ -806,6 +834,18 @@ msgstr "Rešetka"
msgid "Health"
msgstr "Zdravlje"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Nadzor heartbeata"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat uspješno poslan"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew naredba"
msgid "Host / IP"
msgstr "Host / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP metoda"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metoda: POST, GET ili HEAD (zadano: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Slika"
msgid "Inactive"
msgstr "Neaktivno"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Interval"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Nevažeća email adresa."
@@ -958,7 +1010,7 @@ msgstr "Iskorištenost memorije Docker spremnika"
#: src/components/routes/system/smart-table.tsx
msgid "Model"
msgstr ""
msgstr "Model"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
@@ -1110,6 +1162,10 @@ msgstr "Pauzirano"
msgid "Paused ({pausedSystemsLength})"
msgstr "Pauzirano ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Format korisnog tereta (Payload)"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "Pretraži"
msgid "Search for systems or settings..."
msgstr "Pretraži za sisteme ili postavke..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Sekunde između pingova (zadano: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način primanja upozorenja."
@@ -1315,6 +1375,18 @@ msgstr "Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način prim
msgid "Select {foo}"
msgstr "Odaberi {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Pošaljite jedan heartbeat ping kako biste provjerili radi li vaša krajnja točka."
#: 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 "Šaljite povremene odlazne pingove vanjskoj usluzi nadzora kako biste mogli nadzirati Beszel bez izlaganja internetu."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Pošalji testni heartbeat"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Poslano"
@@ -1335,6 +1407,10 @@ msgstr "Usluge"
msgid "Set percentage thresholds for meter colors."
msgstr "Postavite pragove postotka za boje mjerača."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Postavite sljedeće varijable okruženja na svom Beszel hubu kako biste omogućili nadzor heartbeata:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Temperature sistemskih senzora"
msgid "Test <0>URL</0>"
msgstr "Testni <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Testiraj heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Testna obavijest poslana"
#: 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 "Ukupni status je <0>ok</0> kada su svi sustavi u radu, <1>warn</1> kada su aktivirana upozorenja i <2>error</2> kada je bilo koji sustav isključen."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Zatim se prijavite u backend i resetirajte lozinku korisničkog računa u tablici korisnika."
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "Otpremi"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Vrijeme rada"
msgstr "Uptime"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Push obavijest"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Kada je omogućen, ovaj token omogućuje agentima da se sami registriraju bez prethodnog stvaranja sustava."
#: 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 "Kada koristite POST, svaki heartbeat uključuje JSON payload sa sažetkom statusa sustava, popisom isključenih sustava i aktiviranim upozorenjima."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Műveletek"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktív"
@@ -140,6 +141,10 @@ msgstr "Adminisztráció"
msgid "After"
msgstr "Utána"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "A környezeti változók beállítása után indítsa újra a Beszel hubot a módosítások érvénybe léptetéséhez."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Ügynök"
@@ -350,6 +355,10 @@ msgstr "Ellenőrizd a {email} címet a visszaállító linkért."
msgid "Check logs for more details."
msgstr "Ellenőrizd a naplót a további részletekért."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Ellenőrizze a megfigyelő szolgáltatást"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Ellenőrizd az értesítési szolgáltatásodat"
@@ -643,6 +652,14 @@ msgstr "Üres"
msgid "End Time"
msgstr "Befejezés ideje"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "Végpont URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "Pingelendő végpont URL (kötelező)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "E-mail cím megadása a jelszó visszaállításához"
@@ -662,6 +679,9 @@ msgstr "Ideiglenes"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Ideiglenes"
msgid "Error"
msgstr "Hiba"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Példa:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Hitelesítés sikertelen"
msgid "Failed to save settings"
msgstr "Nem sikerült menteni a beállításokat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Nem sikerült elküldeni a szívverést (heartbeat)"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Teszt értesítés elküldése sikertelen"
@@ -806,6 +834,18 @@ msgstr "Rács"
msgid "Health"
msgstr "Egészség"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Heartbeat figyelés"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat sikeresen elküldve"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew parancs"
msgid "Host / IP"
msgstr "Állomás / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP metódus"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP metódus: POST, GET vagy HEAD (alapértelmezett: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Kép"
msgid "Inactive"
msgstr "Inaktív"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Intervallum"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Érvénytelen e-mail cím."
@@ -1110,6 +1162,10 @@ msgstr "Szüneteltetve"
msgid "Paused ({pausedSystemsLength})"
msgstr "Szüneteltetve ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Payload formátum"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "Keresés"
msgid "Search for systems or settings..."
msgstr "Keresés rendszerek vagy beállítások után..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Pingek közötti másodpercek (alapértelmezett: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogyan kap értesítéseket."
@@ -1315,6 +1375,18 @@ msgstr "Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogy
msgid "Select {foo}"
msgstr "{foo} kiválasztása"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Küldjön egyetlen heartbeat pinget a végpont működésének ellenőrzéséhez."
#: 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 "Küldjön időszakos kimenő pingeket egy külső megfigyelő szolgáltatásnak, így a Beszel-t az internetnek való kitettség nélkül is megfigyelheti."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Teszt heartbeat küldése"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Elküldve"
@@ -1335,6 +1407,10 @@ msgstr "Szolgáltatások"
msgid "Set percentage thresholds for meter colors."
msgstr "Százalékos küszöbértékek beállítása a mérőszínekhez."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Állítsa be a következő környezeti változókat a Beszel hubon a heartbeat figyelés engedélyezéséhez:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "A rendszer érzékelőinek hőmérséklete"
msgid "Test <0>URL</0>"
msgstr "Teszt <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Teszt heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Teszt értesítés elküldve"
#: 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 "Az összesített állapot <0>ok</0>, ha minden rendszer fut, <1>warn</1>, ha riasztások léptek fel, és <2>error</2>, ha bármelyik rendszer leállt."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Ezután jelentkezzen be a backendbe, és állítsa vissza a felhasználói fiók jelszavát a felhasználók táblázatban."
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "Feltöltés"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Üzemidő"
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Push értesítések"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Ha engedélyezve van, ez a token lehetővé teszi az ügynökök számára a regisztrációt a rendszer előzetes létrehozása nélkül."
#: 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 használata esetén minden heartbeat tartalmaz egy JSON payload-ot a rendszerállapot összefoglalójával, a leállt rendszerek listájával és a kiváltott riasztásokkal."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Aksi"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktif"
@@ -140,6 +141,10 @@ msgstr "Admin"
msgid "After"
msgstr "Setelah"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Setelah mengatur variabel lingkungan, restart hub Beszel Anda agar perubahan dapat diterapkan."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agen"
@@ -350,6 +355,10 @@ msgstr "Periksa {email} untuk tautan atur ulang password."
msgid "Check logs for more details."
msgstr "Periksa riwayat untuk lebih detail."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Periksa layanan pemantauan Anda"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Periksa jasa penyedia notifikasi anda"
@@ -457,7 +466,7 @@ msgstr "Salin YAML"
#: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr ""
msgstr "CPU"
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
@@ -543,7 +552,7 @@ msgstr "Deskripsi"
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
msgstr ""
msgstr "Detail"
#: src/components/routes/system/smart-table.tsx
msgid "Device"
@@ -556,11 +565,11 @@ msgstr "Sedang tidak di charge"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Disk"
msgstr ""
msgstr "Disk"
#: src/components/routes/system.tsx
msgid "Disk I/O"
msgstr ""
msgstr "Disk I/O"
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
@@ -586,7 +595,7 @@ msgstr "Penggunaan Memori Docker"
#: src/components/routes/system.tsx
msgid "Docker Network I/O"
msgstr ""
msgstr "Docker Network I/O"
#: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx
@@ -607,7 +616,7 @@ msgstr "Mati ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr ""
msgstr "Unduh"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -627,7 +636,7 @@ msgstr "Ubah {foo}"
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr ""
msgstr "Email"
#: src/components/routes/settings/notifications.tsx
msgid "Email notifications"
@@ -643,6 +652,14 @@ msgstr "Kosong"
msgid "End Time"
msgstr "Waktu Berakhir"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL Titik Akhir"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL titik akhir untuk di-ping (diperlukan)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Masukkan alamat email untuk mereset kata sandi"
@@ -662,13 +679,20 @@ msgstr "Sementara"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
msgstr ""
msgstr "Error"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Contoh:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
@@ -707,7 +731,7 @@ msgstr "Export konfigurasi sistem anda saat ini."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr ""
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
@@ -727,6 +751,10 @@ msgstr "Gagal mengautentikasi"
msgid "Failed to save settings"
msgstr "Gagal menyimpan pengaturan"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Gagal mengirim heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Gagal mengirim tes notifikasi"
@@ -747,7 +775,7 @@ msgstr "Gagal: {0}"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Filter..."
msgstr ""
msgstr "Saring..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
@@ -755,7 +783,7 @@ msgstr "Sidik jari"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr ""
msgstr "Firmware"
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -784,7 +812,7 @@ msgstr "Umum"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr ""
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
@@ -806,6 +834,18 @@ msgstr "Kartu"
msgid "Health"
msgstr "Kesehatan"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Pemantauan Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat berhasil dikirim"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -814,12 +854,20 @@ msgstr "Perintah Homebrew"
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr ""
msgstr "Host / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "Metode HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Metode HTTP: POST, GET, atau HEAD (default: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr ""
msgstr "Idle"
#: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command."
@@ -828,12 +876,16 @@ 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 "Image"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Tidak aktif"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Interval"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Alamat email tidak valid."
@@ -958,7 +1010,7 @@ msgstr "Penggunaan memori kontainer docker"
#: src/components/routes/system/smart-table.tsx
msgid "Model"
msgstr ""
msgstr "Model"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
@@ -1110,6 +1162,10 @@ msgstr "Dijeda"
msgid "Paused ({pausedSystemsLength})"
msgstr "Dijeda ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Format payload"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1162,7 +1218,7 @@ msgstr "Silakan masuk ke akun anda"
#: src/components/add-system.tsx
msgid "Port"
msgstr ""
msgstr "Port"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1248,7 +1304,7 @@ msgstr "Lanjutkan"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr ""
msgstr "Root"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1307,6 +1363,10 @@ msgstr "Cari"
msgid "Search for systems or settings..."
msgstr "Cari sistem atau pengaturan..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Detik di antara ping (default: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Lihat <0>pengaturan notifikasi</0> untuk mengkonfigurasi bagaimana anda menerima peringatan."
@@ -1315,6 +1375,18 @@ msgstr "Lihat <0>pengaturan notifikasi</0> untuk mengkonfigurasi bagaimana anda
msgid "Select {foo}"
msgstr "Pilih {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Kirim satu ping heartbeat untuk memverifikasi titik akhir Anda berfungsi."
#: 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 "Kirim ping keluar secara berkala ke layanan pemantauan eksternal sehingga Anda dapat memantau Beszel tanpa mengeksposnya ke internet."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Kirim tes heartbeat"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Dikirim"
@@ -1335,6 +1407,10 @@ msgstr "Layanan"
msgid "Set percentage thresholds for meter colors."
msgstr "Tetapkan ambang persentase untuk warna meter."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Setel variabel lingkungan berikut di hub Beszel Anda untuk mengaktifkan pemantauan heartbeat:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1377,7 +1453,7 @@ msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr ""
msgstr "Status"
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Sub State"
@@ -1452,10 +1528,18 @@ msgstr "Temperatur sensor sistem"
msgid "Test <0>URL</0>"
msgstr "Tes <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Tes heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Notifikasi tes dikirim"
#: 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 "Status keseluruhan adalah <0>ok</0> ketika semua sistem aktif, <1>warn</1> ketika peringatan dipicu, dan <2>error</2> ketika ada sistem yang mati."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Kemudian masuk ke backend dan reset kata sandi akun pengguna anda di tabel pengguna."
@@ -1497,7 +1581,7 @@ msgstr "Ganti tema"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr ""
msgstr "Token"
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1516,7 +1600,7 @@ msgstr "Token dan Fingerprint digunakan untuk mengautentikasi koneksi WebSocket
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr ""
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
@@ -1529,7 +1613,7 @@ msgstr "Total data yang dikirim untuk setiap antarmuka"
#. placeholder {0}: data.length
#: src/components/systemd-table/systemd-table.tsx
msgid "Total: {0}"
msgstr ""
msgstr "Total: {0}"
#: src/components/systemd-table/systemd-table.tsx
msgid "Triggered by"
@@ -1639,9 +1723,10 @@ msgstr "Diperbarui setiap 10 menit."
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr ""
msgstr "Unggah"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Waktu aktif"
@@ -1716,6 +1801,10 @@ msgstr "Webhook / Push notifikasi"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Ketika diaktifkan, token ini memungkinkan agen untuk mendaftar sendiri tanpa pembuatan sistem."
#: 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 "Saat menggunakan POST, setiap heartbeat menyertakan payload JSON dengan ringkasan status sistem, daftar sistem yang mati, dan peringatan yang dipicu."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "Azioni"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Attivo"
@@ -140,6 +141,10 @@ msgstr "Amministratore"
msgid "After"
msgstr "Dopo"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Dopo aver impostato le variabili d'ambiente, riavvia il tuo Beszel hub affinché le modifiche abbiano effetto."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agente"
@@ -231,7 +236,7 @@ msgstr "Larghezza di banda"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr ""
msgstr "Bat"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -350,6 +355,10 @@ msgstr "Controlla {email} per un link di reset."
msgid "Check logs for more details."
msgstr "Controlla i log per maggiori dettagli."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "Controlla il tuo servizio di monitoraggio"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Controlla il tuo servizio di notifica"
@@ -643,6 +652,14 @@ msgstr "Vuota"
msgid "End Time"
msgstr "Ora di fine"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "URL dell'endpoint"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "URL dell'endpoint da pingare (richiesto)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Inserisci l'indirizzo email per reimpostare la password"
@@ -662,6 +679,9 @@ msgstr "Effimero"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "Effimero"
msgid "Error"
msgstr "Errore"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "Esempio:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "Autenticazione fallita"
msgid "Failed to save settings"
msgstr "Salvataggio delle impostazioni fallito"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Invio heartbeat fallito"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Invio della notifica di test fallito"
@@ -806,6 +834,18 @@ msgstr "Griglia"
msgid "Health"
msgstr "Stato"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "Monitoraggio Heartbeat"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "Heartbeat inviato con successo"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Comando Homebrew"
msgid "Host / IP"
msgstr "Host / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "Metodo HTTP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "Metodo HTTP: POST, GET o HEAD (predefinito: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "Immagine"
msgid "Inactive"
msgstr "Inattivo"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "Intervallo"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Indirizzo email non valido."
@@ -993,7 +1045,7 @@ msgstr "Unità rete"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "No"
msgstr ""
msgstr "No"
#: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx
@@ -1110,6 +1162,10 @@ msgstr "In pausa"
msgid "Paused ({pausedSystemsLength})"
msgstr "In pausa ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Formato del payload"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "Cerca"
msgid "Search for systems or settings..."
msgstr "Cerca sistemi o impostazioni..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "Secondi tra i ping (predefinito: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Vedi <0>impostazioni di notifica</0> per configurare come ricevere gli avvisi."
@@ -1315,6 +1375,18 @@ msgstr "Vedi <0>impostazioni di notifica</0> per configurare come ricevere gli a
msgid "Select {foo}"
msgstr "Seleziona {foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "Invia un singolo ping di heartbeat per verificare che l'endpoint funzioni."
#: 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 "Invia ping in uscita periodici a un servizio di monitoraggio esterno in modo da poter monitorare Beszel senza esporlo a Internet."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "Invia heartbeat di prova"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Inviato"
@@ -1335,6 +1407,10 @@ msgstr "Servizi"
msgid "Set percentage thresholds for meter colors."
msgstr "Imposta le soglie percentuali per i colori dei contatori."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "Imposta le seguenti variabili d'ambiente sul tuo Beszel hub per abilitare il monitoraggio heartbeat:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "Temperature dei sensori di sistema"
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "Test heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
msgstr "Notifica di test inviata"
#: 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 "Lo stato generale è <0>ok</0> quando tutti i sistemi sono attivi, <1>avviso</1> quando gli avvisi sono attivati e <2>errore</2> quando un sistema è inattivo."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Quindi accedi al backend e reimposta la password del tuo account utente nella tabella degli utenti."
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "Carica"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "Tempo di attività"
msgstr "Uptime"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Notifiche Webhook / Push"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "Quando abilitato, questo token consente agli agenti di registrarsi automaticamente senza creazione preventiva del sistema."
#: 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 "Quando si usa POST, ogni heartbeat include un payload JSON con il riepilogo dello stato del sistema, l'elenco dei sistemi inattivi e gli avvisi attivati."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "アクション"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "アクティブ"
@@ -140,6 +141,10 @@ msgstr "管理者"
msgid "After"
msgstr "後"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "環境変数を設定した後、変更を有効にするために Beszel ハブを再起動してください。"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "エージェント"
@@ -350,6 +355,10 @@ msgstr "{email}を確認してリセットリンクを探してください。"
msgid "Check logs for more details."
msgstr "詳細についてはログを確認してください。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "監視サービスを確認する"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "通知サービスを確認してください"
@@ -457,7 +466,7 @@ msgstr "YAMLをコピー"
#: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr ""
msgstr "CPU"
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
@@ -643,6 +652,14 @@ msgstr "空"
msgid "End Time"
msgstr "終了時間"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "エンドポイント URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "ping するエンドポイント URL (必須)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "パスワードをリセットするためにメールアドレスを入力してください"
@@ -662,6 +679,9 @@ msgstr "一時的"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "一時的"
msgid "Error"
msgstr "エラー"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "例:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -727,6 +751,10 @@ msgstr "認証に失敗しました"
msgid "Failed to save settings"
msgstr "設定の保存に失敗しました"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "ハートビートの送信に失敗しました"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "テスト通知の送信に失敗しました"
@@ -806,6 +834,18 @@ msgstr "グリッド"
msgid "Health"
msgstr "ヘルス"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "ハートビート"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "ハートビート監視"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "ハートビートが正常に送信されました"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew コマンド"
msgid "Host / IP"
msgstr "ホスト / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP メソッド"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP メソッド: POST、GET、または HEAD (デフォルト: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "イメージ"
msgid "Inactive"
msgstr "非アクティブ"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "間隔"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "無効なメールアドレスです。"
@@ -1110,6 +1162,10 @@ msgstr "一時停止中"
msgid "Paused ({pausedSystemsLength})"
msgstr "一時停止 ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "ペイロード形式"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1307,6 +1363,10 @@ msgstr "検索"
msgid "Search for systems or settings..."
msgstr "システムまたは設定を検索..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "ping 間の秒数 (デフォルト: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "アラートの受信方法を設定するには<0>通知設定</0>を参照してください。"
@@ -1315,6 +1375,18 @@ msgstr "アラートの受信方法を設定するには<0>通知設定</0>を
msgid "Select {foo}"
msgstr "{foo}を選択"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "エンドポイントが機能していることを確認するために、単一のハートビート ping を送信します。"
#: 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 をインターネットに公開せずに監視できます。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "テストハートビートを送信"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "送信"
@@ -1335,6 +1407,10 @@ msgstr "サービス"
msgid "Set percentage thresholds for meter colors."
msgstr "メーターの色を変更するしきい値(パーセンテージ)を設定します。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "ハートビート監視を有効にするには、Beszel ハブで次の環境変数を設定します。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "システムセンサーの温度"
msgid "Test <0>URL</0>"
msgstr "テスト<0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "テストハートビート"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
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> になります。"
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "その後、バックエンドにログインして、ユーザーテーブルでユーザーアカウントのパスワードをリセットしてください。"
@@ -1642,6 +1726,7 @@ msgid "Upload"
msgstr "アップロード"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "稼働時間"
@@ -1716,6 +1801,10 @@ msgstr "Webhook / プッシュ通知"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "有効にすると、このトークンによりエージェントは事前のシステム作成なしで自己登録できます。"
#: 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 を使用する場合、各ハートビートには、システムステータスの概要、ダウンしているシステムのリスト、およびトリガーされたアラートを含む JSON ペイロードが含まれます。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

View File

@@ -93,6 +93,7 @@ msgstr "작업"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "활성"
@@ -140,6 +141,10 @@ msgstr "관리자"
msgid "After"
msgstr "이후"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "환경 변수를 설정한 후, 변경 사항을 적용하려면 Beszel 허브를 재시작하세요."
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "에이전트"
@@ -350,6 +355,10 @@ msgstr "{email}에서 재설정 링크를 확인하세요."
msgid "Check logs for more details."
msgstr "자세한 내용은 로그를 확인하세요."
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
msgstr "모니터링 서비스 확인"
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "알림 서비스를 확인하세요."
@@ -643,6 +652,14 @@ msgstr "빔"
msgid "End Time"
msgstr "종료 시간"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "엔드포인트 URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "핑을 보낼 엔드포인트 URL (필수)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "비밀번호를 재설정하려면 이메일 주소를 입력하세요"
@@ -662,6 +679,9 @@ msgstr "일시적"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/heartbeat.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -670,6 +690,10 @@ msgstr "일시적"
msgid "Error"
msgstr "오류"
#: src/components/routes/settings/heartbeat.tsx
msgid "Example:"
msgstr "예시:"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
#. placeholder {2}: alert.min
@@ -679,7 +703,7 @@ msgstr "마지막 {2, plural, one {# 분} other {# 분}} 동안 {0}{1} 초과"
#: src/components/systemd-table/systemd-table.tsx
msgid "Exec main PID"
msgstr ""
msgstr "실행 메인 PID"
#: src/components/routes/settings/config-yaml.tsx
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
@@ -727,6 +751,10 @@ msgstr "인증 실패"
msgid "Failed to save settings"
msgstr "설정 저장 실패"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "하트비트 전송 실패"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "테스트 알림 전송 실패"
@@ -806,6 +834,18 @@ msgstr "그리드"
msgid "Health"
msgstr "상태"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "하트비트"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
msgstr "하트비트 모니터링"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat sent successfully"
msgstr "하트비트 전송 성공"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -816,6 +856,14 @@ msgstr "Homebrew 명령어"
msgid "Host / IP"
msgstr "호스트 / IP"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
msgstr "HTTP 메서드"
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP method: POST, GET, or HEAD (default: POST)"
msgstr "HTTP 메서드: POST, GET 또는 HEAD (기본값: POST)"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
@@ -834,6 +882,10 @@ msgstr "이미지"
msgid "Inactive"
msgstr "비활성"
#: src/components/routes/settings/heartbeat.tsx
msgid "Interval"
msgstr "간격"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "잘못된 이메일 주소입니다."
@@ -858,7 +910,7 @@ msgstr "생명주기"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "limit"
msgstr ""
msgstr "제한"
#: src/components/routes/system.tsx
msgid "Load Average"
@@ -914,7 +966,7 @@ msgstr "알림을 생성하려 하시나요? 시스템 테이블의 종 <0/> 아
#: src/components/systemd-table/systemd-table.tsx
msgid "Main PID"
msgstr ""
msgstr "메인 PID"
#: src/components/routes/settings/layout.tsx
msgid "Manage display and notification preferences."
@@ -1110,6 +1162,10 @@ msgstr "일시 정지됨"
msgid "Paused ({pausedSystemsLength})"
msgstr "일시 정지됨 ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "페이로드 형식"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
@@ -1248,7 +1304,7 @@ msgstr "재개"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr ""
msgstr "루트"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1307,6 +1363,10 @@ msgstr "검색"
msgid "Search for systems or settings..."
msgstr "시스템 또는 설정 검색..."
#: src/components/routes/settings/heartbeat.tsx
msgid "Seconds between pings (default: 60)"
msgstr "핑 사이 시간(초) (기본값: 60)"
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "알림을 받는 방법을 구성하려면 <0>알림 설정</0>을 참조하세요."
@@ -1315,6 +1375,18 @@ msgstr "알림을 받는 방법을 구성하려면 <0>알림 설정</0>을 참
msgid "Select {foo}"
msgstr "{foo} 선택"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "엔드포인트가 작동하는지 확인하기 위해 단일 하트비트 핑을 보냅니다."
#: 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 "외부 모니터링 서비스에 주기적으로 아웃바운드 핑을 보내 인터넷에 노출하지 않고도 Beszel을 모니터링할 수 있습니다."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "테스트 하트비트 전송"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "보냄"
@@ -1335,6 +1407,10 @@ msgstr "서비스"
msgid "Set percentage thresholds for meter colors."
msgstr "그래프 미터 색상의 백분율 임계값을 설정합니다."
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "하트비트 모니터링을 활성화하려면 Beszel 허브에 다음 환경 변수를 설정하세요:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -1452,10 +1528,18 @@ msgstr "시스템 센서의 온도"
msgid "Test <0>URL</0>"
msgstr "테스트 <0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "테스트 하트비트"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
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> 상태가 됩니다."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "그런 다음 백엔드에 로그인하여 사용자 테이블에서 사용자 계정 비밀번호를 재설정하세요."
@@ -1591,7 +1675,7 @@ msgstr "유형"
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr ""
msgstr "유닛 파일"
#. Temperature / network units
#: src/components/routes/settings/general.tsx
@@ -1642,8 +1726,9 @@ msgid "Upload"
msgstr "업로드"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Uptime"
msgstr "가동 시간"
msgstr "가동시간"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1716,6 +1801,10 @@ msgstr "Webhook / 푸시 알림"
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "활성화되면 이 토큰은 사전 시스템 생성 없이 에이전트가 자체 등록할 수 있도록 합니다."
#: 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를 사용할 때 각 하트비트에는 시스템 상태 요약, 다운된 시스템 목록 및 트리거된 알림이 포함된 JSON 페이로드가 포함됩니다."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"

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