Compare commits

..
Author SHA1 Message Date
Jaypalsinh BaradandGitHub 734c966307 Implement container health alerts (#1679)
* Implement container health alerts, including a database migration for alert names and UI updates.

* Add bulk container alerts and improve alerting logic

* Implement container-specific alerts with new database collections, API endpoints, and UI components.
2026-09-02 13:19:40 -04:00
71 changed files with 1841 additions and 961 deletions
+33 -3
View File
@@ -5,8 +5,11 @@
package agent
import (
"crypto/sha256"
"encoding/hex"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -16,6 +19,7 @@ import (
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/host"
gossh "golang.org/x/crypto/ssh"
)
@@ -61,7 +65,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
agent.netIoStats = make(map[uint16]system.NetIoStats)
agent.netInterfaceDeltaTrackers = make(map[uint16]*deltatracker.DeltaTracker[string, uint64])
agent.dataDir, err = GetDataDir(dataDir...)
agent.dataDir, err = getDataDir(dataDir...)
if err != nil {
slog.Warn("Data directory not found")
} else {
@@ -224,12 +228,38 @@ func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedD
return data
}
// Start initializes and starts the agent with optional WebSocket connection
// StartAgent initializes and starts the agent with optional WebSocket connection
func (a *Agent) Start(serverOptions ServerOptions) error {
a.keys = serverOptions.Keys
return a.connectionManager.Start(serverOptions)
}
func (a *Agent) getFingerprint() string {
return GetFingerprint(a.dataDir, a.systemDetails.Hostname, a.systemDetails.CpuModel)
// first look for a fingerprint in the data directory
if a.dataDir != "" {
if fp, err := os.ReadFile(filepath.Join(a.dataDir, "fingerprint")); err == nil {
return string(fp)
}
}
// if no fingerprint is found, generate one
fingerprint, err := host.HostID()
// we ignore a commonly known "product_uuid" known not to be unique
if err != nil || fingerprint == "" || fingerprint == "03000200-0400-0500-0006-000700080009" {
fingerprint = a.systemDetails.Hostname + a.systemDetails.CpuModel
}
// hash fingerprint
sum := sha256.Sum256([]byte(fingerprint))
fingerprint = hex.EncodeToString(sum[:24])
// save fingerprint to data directory
if a.dataDir != "" {
err = os.WriteFile(filepath.Join(a.dataDir, "fingerprint"), []byte(fingerprint), 0644)
if err != nil {
slog.Warn("Failed to save fingerprint", "err", err)
}
}
return fingerprint
}
+2 -2
View File
@@ -8,10 +8,10 @@ import (
"runtime"
)
// GetDataDir returns the path to the data directory for the agent and an error
// getDataDir returns the path to the data directory for the agent and an error
// if the directory is not valid. Attempts to find the optimal data directory if
// no data directories are provided.
func GetDataDir(dataDirs ...string) (string, error) {
func getDataDir(dataDirs ...string) (string, error) {
if len(dataDirs) > 0 {
return testDataDirs(dataDirs)
}
+5 -5
View File
@@ -17,7 +17,7 @@ func TestGetDataDir(t *testing.T) {
// Test with explicit dataDir parameter
t.Run("explicit data dir", func(t *testing.T) {
tempDir := t.TempDir()
result, err := GetDataDir(tempDir)
result, err := getDataDir(tempDir)
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
@@ -26,7 +26,7 @@ func TestGetDataDir(t *testing.T) {
t.Run("explicit data dir - create new", func(t *testing.T) {
tempDir := t.TempDir()
newDir := filepath.Join(tempDir, "new-data-dir")
result, err := GetDataDir(newDir)
result, err := getDataDir(newDir)
require.NoError(t, err)
assert.Equal(t, newDir, result)
@@ -52,7 +52,7 @@ func TestGetDataDir(t *testing.T) {
os.Setenv("BESZEL_AGENT_DATA_DIR", tempDir)
result, err := GetDataDir()
result, err := getDataDir()
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
@@ -60,7 +60,7 @@ func TestGetDataDir(t *testing.T) {
// Test with invalid explicit dataDir
t.Run("invalid explicit data dir", func(t *testing.T) {
invalidPath := "/invalid/path/that/cannot/be/created"
_, err := GetDataDir(invalidPath)
_, err := getDataDir(invalidPath)
assert.Error(t, err)
})
@@ -79,7 +79,7 @@ func TestGetDataDir(t *testing.T) {
// This will try platform-specific defaults, which may or may not work
// We're mainly testing that it doesn't panic and returns some result
result, err := GetDataDir()
result, err := getDataDir()
// We don't assert success/failure here since it depends on system permissions
// Just verify we get a string result if no error
if err == nil {
+22 -28
View File
@@ -26,15 +26,6 @@ func parseFilesystemEntry(entry string) (device, customName string) {
return device, customName
}
func isDockerSpecialMountpoint(mountpoint string) bool {
switch mountpoint {
case "/etc/hosts", "/etc/resolv.conf", "/etc/hostname":
return true
default:
return false
}
}
// Sets up the filesystems to monitor for disk usage and I/O.
func (a *Agent) initializeDiskInfo() {
filesystem, _ := GetEnv("FILESYSTEM")
@@ -78,15 +69,11 @@ 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.
// Check if root device is in /proc/diskstats, use fallback if not
if _, ioMatch = diskIoCounters[key]; !ioMatch {
if matchedKey, match := findIoDevice(filesystem, diskIoCounters); match {
key = matchedKey
ioMatch = true
} else {
slog.Warn("Root I/O unmapped; set FILESYSTEM", "device", device, "mountpoint", mountpoint)
key, ioMatch = findIoDevice(filesystem, diskIoCounters, a.fsStats)
if !ioMatch {
slog.Info("Using I/O fallback", "device", device, "mountpoint", mountpoint, "fallback", key)
}
}
} else {
@@ -154,8 +141,8 @@ func (a *Agent) initializeDiskInfo() {
for _, p := range partitions {
// fmt.Println(p.Device, p.Mountpoint)
// Binary root fallback or docker root fallback
if !hasRoot && (p.Mountpoint == rootMountPoint || (isDockerSpecialMountpoint(p.Mountpoint) && strings.HasPrefix(p.Device, "/dev"))) {
fs, match := findIoDevice(filepath.Base(p.Device), diskIoCounters)
if !hasRoot && (p.Mountpoint == rootMountPoint || (p.Mountpoint == "/etc/hosts" && strings.HasPrefix(p.Device, "/dev"))) {
fs, match := findIoDevice(filepath.Base(p.Device), diskIoCounters, a.fsStats)
if match {
addFsStat(fs, p.Mountpoint, true)
hasRoot = true
@@ -189,26 +176,33 @@ func (a *Agent) initializeDiskInfo() {
// If no root filesystem set, use fallback
if !hasRoot {
rootKey := filepath.Base(rootMountPoint)
if _, exists := a.fsStats[rootKey]; exists {
rootKey = "root"
}
slog.Warn("Root device not detected; root I/O disabled", "mountpoint", rootMountPoint)
a.fsStats[rootKey] = &system.FsStats{Root: true, Mountpoint: rootMountPoint}
rootDevice, _ := findIoDevice(filepath.Base(filesystem), diskIoCounters, a.fsStats)
slog.Info("Root disk", "mountpoint", rootMountPoint, "io", rootDevice)
a.fsStats[rootDevice] = &system.FsStats{Root: true, Mountpoint: rootMountPoint}
}
a.initializeDiskIoStats(diskIoCounters)
}
// Returns matching device from /proc/diskstats.
// Returns matching device from /proc/diskstats,
// or the device with the most reads if no match is found.
// bool is true if a match was found.
func findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCountersStat) (string, bool) {
func findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCountersStat, fsStats map[string]*system.FsStats) (string, bool) {
var maxReadBytes uint64
maxReadDevice := "/"
for _, d := range diskIoCounters {
if d.Name == filesystem || (d.Label != "" && d.Label == filesystem) {
return d.Name, true
}
if d.ReadBytes > maxReadBytes {
// don't use if device already exists in fsStats
if _, exists := fsStats[d.Name]; !exists {
maxReadBytes = d.ReadBytes
maxReadDevice = d.Name
}
}
}
return "", false
return maxReadDevice, false
}
// Sets start values for disk I/O stats.
-56
View File
@@ -94,62 +94,6 @@ func TestParseFilesystemEntry(t *testing.T) {
}
}
func TestFindIoDevice(t *testing.T) {
t.Run("matches by device name", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda"},
"sdb": {Name: "sdb"},
}
device, ok := findIoDevice("sdb", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sdb", device)
})
t.Run("matches by device label", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda", Label: "rootfs"},
"sdb": {Name: "sdb"},
}
device, ok := findIoDevice("rootfs", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
t.Run("returns no fallback when not found", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda"},
"sdb": {Name: "sdb"},
}
device, ok := findIoDevice("nvme0n1p1", ioCounters)
assert.False(t, ok)
assert.Equal(t, "", device)
})
}
func TestIsDockerSpecialMountpoint(t *testing.T) {
testCases := []struct {
name string
mountpoint string
expected bool
}{
{name: "hosts", mountpoint: "/etc/hosts", expected: true},
{name: "resolv", mountpoint: "/etc/resolv.conf", expected: true},
{name: "hostname", mountpoint: "/etc/hostname", expected: true},
{name: "root", mountpoint: "/", expected: false},
{name: "passwd", mountpoint: "/etc/passwd", expected: false},
{name: "extra-filesystem", mountpoint: "/extra-filesystems/sda1", expected: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, isDockerSpecialMountpoint(tc.mountpoint))
})
}
}
func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
// Set up environment variables
oldEnv := os.Getenv("EXTRA_FILESYSTEMS")
-56
View File
@@ -32,10 +32,6 @@ var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*
const (
// Docker API timeout in milliseconds
dockerTimeoutMs = 2100
// Number of consecutive /containers/json failures before forcing a client reset on old Docker versions
dockerClientResetFailureThreshold = 3
// Minimum time between Docker client resets to avoid reset flapping
dockerClientResetCooldown = 30 * time.Second
// Maximum realistic network speed (5 GB/s) to detect bad deltas
maxNetworkSpeedBps uint64 = 5e9
// Maximum conceivable memory usage of a container (100TB) to detect bad memory stats
@@ -59,16 +55,12 @@ type dockerManager struct {
containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
versionChecked bool // Whether docker version detection completed successfully
isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
transport *http.Transport // Base transport used by client for connection resets
consecutiveListFailures int // Number of consecutive /containers/json request failures
lastClientReset time.Time // Last time the Docker client connections were reset
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
// Maps cache time intervals to container-specific CPU usage tracking
@@ -127,10 +119,8 @@ func (dm *dockerManager) shouldExcludeContainer(name string) bool {
func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats, error) {
resp, err := dm.client.Get("http://localhost/containers/json")
if err != nil {
dm.handleContainerListError(err)
return nil, err
}
dm.consecutiveListFailures = 0
dm.apiContainerList = dm.apiContainerList[:0]
if err := dm.decode(resp, &dm.apiContainerList); err != nil {
@@ -214,50 +204,6 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats,
return stats, nil
}
func (dm *dockerManager) handleContainerListError(err error) {
dm.consecutiveListFailures++
if !dm.shouldResetDockerClient(err) {
return
}
dm.resetDockerClientConnections()
}
func (dm *dockerManager) shouldResetDockerClient(err error) bool {
if !dm.versionChecked || dm.goodDockerVersion {
return false
}
if dm.consecutiveListFailures < dockerClientResetFailureThreshold {
return false
}
if !dm.lastClientReset.IsZero() && time.Since(dm.lastClientReset) < dockerClientResetCooldown {
return false
}
return isDockerApiOverloadError(err)
}
func isDockerApiOverloadError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
msg := err.Error()
return strings.Contains(msg, "Client.Timeout exceeded") ||
strings.Contains(msg, "request canceled") ||
strings.Contains(msg, "context deadline exceeded") ||
strings.Contains(msg, "EOF")
}
func (dm *dockerManager) resetDockerClientConnections() {
if dm.transport == nil {
return
}
dm.transport.CloseIdleConnections()
dm.lastClientReset = time.Now()
slog.Warn("Reset Docker client connections after repeated /containers/json failures", "failures", dm.consecutiveListFailures)
}
// initializeCpuTracking initializes CPU tracking maps for a specific cache time interval
func (dm *dockerManager) initializeCpuTracking(cacheTimeMs uint16) {
// Initialize cache time maps if they don't exist
@@ -607,7 +553,6 @@ func newDockerManager() *dockerManager {
Timeout: timeout,
Transport: userAgentTransport,
},
transport: transport,
containerStatsMap: make(map[string]*container.Stats),
sem: make(chan struct{}, 5),
apiContainerList: []*container.ApiInfo{},
@@ -666,7 +611,6 @@ func (dm *dockerManager) checkDockerVersion() {
if err := dm.decode(resp, &versionInfo); err != nil {
return
}
dm.versionChecked = true
// if version > 24, one-shot works correctly and we can limit concurrent operations
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
dm.goodDockerVersion = true
-87
View File
@@ -1,87 +0,0 @@
package agent
import (
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"path/filepath"
"strings"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/host"
)
const fingerprintFileName = "fingerprint"
// knownBadUUID is a commonly known "product_uuid" that is not unique across systems.
const knownBadUUID = "03000200-0400-0500-0006-000700080009"
// GetFingerprint returns the agent fingerprint. It first tries to read a saved
// fingerprint from the data directory. If not found (or dataDir is empty), it
// generates one from system properties. The hostname and cpuModel parameters are
// used as fallback material if host.HostID() fails. If either is empty, they
// are fetched from the system automatically.
//
// If a new fingerprint is generated and a dataDir is provided, it is saved.
func GetFingerprint(dataDir, hostname, cpuModel string) string {
if dataDir != "" {
if fp, err := readFingerprint(dataDir); err == nil {
return fp
}
}
fp := generateFingerprint(hostname, cpuModel)
if dataDir != "" {
_ = SaveFingerprint(dataDir, fp)
}
return fp
}
// generateFingerprint creates a fingerprint from system properties.
// It tries host.HostID() first, falling back to hostname + cpuModel.
// If hostname or cpuModel are empty, they are fetched from the system.
func generateFingerprint(hostname, cpuModel string) string {
fingerprint, err := host.HostID()
if err != nil || fingerprint == "" || fingerprint == knownBadUUID {
if hostname == "" {
hostname, _ = os.Hostname()
}
if cpuModel == "" {
if info, err := cpu.Info(); err == nil && len(info) > 0 {
cpuModel = info[0].ModelName
}
}
fingerprint = hostname + cpuModel
}
sum := sha256.Sum256([]byte(fingerprint))
return hex.EncodeToString(sum[:24])
}
// readFingerprint reads the saved fingerprint from the data directory.
func readFingerprint(dataDir string) (string, error) {
fp, err := os.ReadFile(filepath.Join(dataDir, fingerprintFileName))
if err != nil {
return "", err
}
s := strings.TrimSpace(string(fp))
if s == "" {
return "", errors.New("fingerprint file is empty")
}
return s, nil
}
// SaveFingerprint writes the fingerprint to the data directory.
func SaveFingerprint(dataDir, fingerprint string) error {
return os.WriteFile(filepath.Join(dataDir, fingerprintFileName), []byte(fingerprint), 0o644)
}
// DeleteFingerprint removes the saved fingerprint file from the data directory.
// Returns nil if the file does not exist (idempotent).
func DeleteFingerprint(dataDir string) error {
err := os.Remove(filepath.Join(dataDir, fingerprintFileName))
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
-103
View File
@@ -1,103 +0,0 @@
//go:build testing
// +build testing
package agent
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetFingerprint(t *testing.T) {
t.Run("reads existing fingerprint from file", func(t *testing.T) {
dir := t.TempDir()
expected := "abc123def456"
err := os.WriteFile(filepath.Join(dir, fingerprintFileName), []byte(expected), 0644)
require.NoError(t, err)
fp := GetFingerprint(dir, "", "")
assert.Equal(t, expected, fp)
})
t.Run("trims whitespace from file", func(t *testing.T) {
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, fingerprintFileName), []byte(" abc123 \n"), 0644)
require.NoError(t, err)
fp := GetFingerprint(dir, "", "")
assert.Equal(t, "abc123", fp)
})
t.Run("generates fingerprint when file does not exist", func(t *testing.T) {
dir := t.TempDir()
fp := GetFingerprint(dir, "", "")
assert.NotEmpty(t, fp)
})
t.Run("generates fingerprint when dataDir is empty", func(t *testing.T) {
fp := GetFingerprint("", "", "")
assert.NotEmpty(t, fp)
})
t.Run("generates consistent fingerprint for same inputs", func(t *testing.T) {
fp1 := GetFingerprint("", "myhost", "mycpu")
fp2 := GetFingerprint("", "myhost", "mycpu")
assert.Equal(t, fp1, fp2)
})
t.Run("prefers saved fingerprint over generated", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, SaveFingerprint(dir, "saved-fp"))
fp := GetFingerprint(dir, "anyhost", "anycpu")
assert.Equal(t, "saved-fp", fp)
})
}
func TestSaveFingerprint(t *testing.T) {
t.Run("saves fingerprint to file", func(t *testing.T) {
dir := t.TempDir()
err := SaveFingerprint(dir, "abc123")
require.NoError(t, err)
content, err := os.ReadFile(filepath.Join(dir, fingerprintFileName))
require.NoError(t, err)
assert.Equal(t, "abc123", string(content))
})
t.Run("overwrites existing fingerprint", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, SaveFingerprint(dir, "old"))
require.NoError(t, SaveFingerprint(dir, "new"))
content, err := os.ReadFile(filepath.Join(dir, fingerprintFileName))
require.NoError(t, err)
assert.Equal(t, "new", string(content))
})
}
func TestDeleteFingerprint(t *testing.T) {
t.Run("deletes existing fingerprint", func(t *testing.T) {
dir := t.TempDir()
fp := filepath.Join(dir, fingerprintFileName)
err := os.WriteFile(fp, []byte("abc123"), 0644)
require.NoError(t, err)
err = DeleteFingerprint(dir)
require.NoError(t, err)
// Verify file is gone
_, err = os.Stat(fp)
assert.True(t, os.IsNotExist(err))
})
t.Run("no error when file does not exist", func(t *testing.T) {
dir := t.TempDir()
err := DeleteFingerprint(dir)
assert.NoError(t, err)
})
}
-3
View File
@@ -36,9 +36,6 @@ var hubVersions map[string]semver.Version
// and begins listening for connections. Returns an error if the server
// is already running or if there's an issue starting the server.
func (a *Agent) StartServer(opts ServerOptions) error {
if disableSSH, _ := GetEnv("DISABLE_SSH"); disableSSH == "true" {
return errors.New("SSH disabled")
}
if a.server != nil {
return errors.New("server already started")
}
-20
View File
@@ -1,6 +1,3 @@
//go:build testing
// +build testing
package agent
import (
@@ -183,23 +180,6 @@ func TestStartServer(t *testing.T) {
}
}
func TestStartServerDisableSSH(t *testing.T) {
os.Setenv("BESZEL_AGENT_DISABLE_SSH", "true")
defer os.Unsetenv("BESZEL_AGENT_DISABLE_SSH")
agent, err := NewAgent("")
require.NoError(t, err)
opts := ServerOptions{
Network: "tcp",
Addr: ":45990",
}
err = agent.StartServer(opts)
assert.Error(t, err)
assert.Contains(t, err.Error(), "SSH disabled")
}
/////////////////////////////////////////////////////////////////
//////////////////// ParseKeys Tests ////////////////////////////
/////////////////////////////////////////////////////////////////
+3 -1
View File
@@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
@@ -19,6 +18,8 @@ import (
"time"
"github.com/henrygd/beszel/internal/entities/smart"
"log/slog"
)
// SmartManager manages data collection for SMART devices
@@ -1124,6 +1125,7 @@ func NewSmartManager() (*SmartManager, error) {
sm.refreshExcludedDevices()
path, err := sm.detectSmartctl()
if err != nil {
slog.Debug(err.Error())
return nil, err
}
slog.Debug("smartctl", "path", path)
+5 -4
View File
@@ -63,9 +63,9 @@ func detectRestarter() restarter {
if path, err := exec.LookPath("rc-service"); err == nil {
return &openRCRestarter{cmd: path}
}
if path, err := exec.LookPath("procd"); err == nil {
return &openWRTRestarter{cmd: path}
}
if path, err := exec.LookPath("procd"); err == nil {
return &openWRTRestarter{cmd: path}
}
if path, err := exec.LookPath("service"); err == nil {
if runtime.GOOS == "freebsd" {
return &freeBSDRestarter{cmd: path}
@@ -79,7 +79,7 @@ func detectRestarter() restarter {
func Update(useMirror bool) error {
exePath, _ := os.Executable()
dataDir, err := GetDataDir()
dataDir, err := getDataDir()
if err != nil {
dataDir = os.TempDir()
}
@@ -125,3 +125,4 @@ func Update(useMirror bool) error {
return nil
}
+1 -1
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.2"
// AppName is the name of the application.
AppName = "beszel"
)
+13 -13
View File
@@ -1,10 +1,10 @@
module github.com/henrygd/beszel
go 1.25.7
go 1.25.5
require (
github.com/blang/semver v3.5.1+incompatible
github.com/coreos/go-systemd/v22 v22.7.0
github.com/coreos/go-systemd/v22 v22.6.0
github.com/distatus/battery v0.11.0
github.com/ebitengine/purego v0.9.1
github.com/fxamacker/cbor/v2 v2.9.0
@@ -13,14 +13,14 @@ require (
github.com/lxzan/gws v1.8.9
github.com/nicholas-fedor/shoutrrr v0.13.1
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.36.2
github.com/shirou/gopsutil/v4 v4.26.1
github.com/pocketbase/pocketbase v0.35.1
github.com/shirou/gopsutil/v4 v4.25.12
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
golang.org/x/sys v0.40.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -34,15 +34,15 @@ require (
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -54,15 +54,15 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/image v0.35.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/image v0.34.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.44.3 // indirect
modernc.org/sqlite v1.43.0 // indirect
)
+32 -30
View File
@@ -9,8 +9,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -33,8 +33,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -53,13 +53,13 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
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=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -69,8 +69,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/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -96,8 +96,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.36.2 h1:mzrxnvXKc3yxKlvZdbwoYXkH8kfIETteD0hWdgj0VI4=
github.com/pocketbase/pocketbase v0.36.2/go.mod h1:71vSF8whUDzC8mcLFE10+Qatf9JQdeOGIRWawOuLLKM=
github.com/pocketbase/pocketbase v0.35.1 h1:Cd5ivUThTw29myY/tYa2cb0elkScBMseG6fExZsIQB8=
github.com/pocketbase/pocketbase v0.35.1/go.mod h1:yQnh1o1Aq6wVuqcmZbRbDmIhc31AME/F5pnPR0Bdtmg=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -105,8 +105,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
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=
@@ -129,18 +129,18 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -159,8 +159,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
@@ -185,8 +185,10 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -195,8 +197,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+2
View File
@@ -105,6 +105,8 @@ func NewAlertManager(app hubLike) *AlertManager {
func (am *AlertManager) bindEvents() {
am.hub.OnRecordAfterUpdateSuccess("alerts").BindFunc(updateHistoryOnAlertUpdate)
am.hub.OnRecordAfterDeleteSuccess("alerts").BindFunc(resolveHistoryOnAlertDelete)
am.hub.OnRecordAfterUpdateSuccess("container_alerts").BindFunc(updateHistoryOnContainerAlertUpdate)
am.hub.OnRecordAfterDeleteSuccess("container_alerts").BindFunc(resolveHistoryOnContainerAlertDelete)
am.hub.OnRecordAfterUpdateSuccess("smart_devices").BindFunc(am.handleSmartDeviceAlert)
}
+116
View File
@@ -117,3 +117,119 @@ func DeleteUserAlerts(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, map[string]any{"success": true, "count": numDeleted})
}
// UpsertUserContainerAlerts handles API request to create or update container alerts for a user
// across multiple containers (POST /api/beszel/user-container-alerts)
func UpsertUserContainerAlerts(e *core.RequestEvent) error {
userID := e.Auth.Id
reqData := struct {
Min uint8 `json:"min"`
Value float64 `json:"value"`
Name string `json:"name"`
Systems []string `json:"systems"`
Containers []string `json:"containers"`
Overwrite bool `json:"overwrite"`
}{}
err := e.BindBody(&reqData)
if err != nil || userID == "" || reqData.Name == "" || len(reqData.Systems) == 0 || len(reqData.Containers) == 0 {
return e.BadRequestError("Bad data", err)
}
containerAlertsCollection, err := e.App.FindCachedCollectionByNameOrId("container_alerts")
if err != nil {
return err
}
err = e.App.RunInTransaction(func(txApp core.App) error {
for _, systemId := range reqData.Systems {
for _, containerId := range reqData.Containers {
// find existing matching alert
alertRecord, err := txApp.FindFirstRecordByFilter(containerAlertsCollection,
"system={:system} && container={:container} && name={:name} && user={:user}",
dbx.Params{"system": systemId, "container": containerId, "name": reqData.Name, "user": userID})
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
// skip if alert already exists and overwrite is not set
if !reqData.Overwrite && alertRecord != nil {
continue
}
// create new alert if it doesn't exist
if alertRecord == nil {
alertRecord = core.NewRecord(containerAlertsCollection)
alertRecord.Set("user", userID)
alertRecord.Set("system", systemId)
alertRecord.Set("container", containerId)
alertRecord.Set("name", reqData.Name)
}
alertRecord.Set("value", reqData.Value)
alertRecord.Set("min", reqData.Min)
if err := txApp.SaveNoValidate(alertRecord); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
return e.JSON(http.StatusOK, map[string]any{"success": true})
}
// DeleteUserContainerAlerts handles API request to delete container alerts for a user
// across multiple containers (DELETE /api/beszel/user-container-alerts)
func DeleteUserContainerAlerts(e *core.RequestEvent) error {
userID := e.Auth.Id
reqData := struct {
AlertName string `json:"name"`
Systems []string `json:"systems"`
Containers []string `json:"containers"`
}{}
err := e.BindBody(&reqData)
if err != nil || userID == "" || reqData.AlertName == "" || len(reqData.Systems) == 0 || len(reqData.Containers) == 0 {
return e.BadRequestError("Bad data", err)
}
var numDeleted uint16
err = e.App.RunInTransaction(func(txApp core.App) error {
for _, systemId := range reqData.Systems {
for _, containerId := range reqData.Containers {
// Find existing alert to delete
alertRecord, err := txApp.FindFirstRecordByFilter("container_alerts",
"system={:system} && container={:container} && name={:name} && user={:user}",
dbx.Params{"system": systemId, "container": containerId, "name": reqData.AlertName, "user": userID})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// alert doesn't exist, continue to next container
continue
}
return err
}
if err := txApp.Delete(alertRecord); err != nil {
return err
}
numDeleted++
}
}
return nil
})
if err != nil {
return err
}
return e.JSON(http.StatusOK, map[string]any{"success": true, "count": numDeleted})
}
+394
View File
@@ -0,0 +1,394 @@
package alerts
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
// ContainerAlertData holds the data for a container alert
type ContainerAlertData struct {
systemRecord *core.Record
containerRecord *core.Record
alertRecord *core.Record
name string
unit string
val float64
threshold float64
triggered bool
time time.Time
count uint8
min uint8
descriptor string // override descriptor in notification body
}
// ContainerAlertStats represents the stats structure for container alerts
type ContainerAlertStats struct {
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
Status string `json:"s"`
Health string `json:"h"`
}
// HandleContainerAlerts processes container alerts for a system
func (am *AlertManager) HandleContainerAlerts(systemRecord *core.Record, data *system.CombinedData) error {
containers := data.Containers
if len(containers) == 0 {
return nil
}
// Get all container alerts for this system
alertRecords, err := am.hub.FindAllRecords("container_alerts",
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
)
if err != nil || len(alertRecords) == 0 {
return nil
}
// Group alerts by container
containerAlerts := make(map[string][]*core.Record)
for _, alertRecord := range alertRecords {
containerId := alertRecord.GetString("container")
containerAlerts[containerId] = append(containerAlerts[containerId], alertRecord)
}
// Process alerts for each container
for containerId, alerts := range containerAlerts {
// Find the container in the current stats
var containerStats *container.Stats
for _, c := range containers {
if c.Id == containerId {
containerStats = c
break
}
}
// If container not found in current stats, it might be stopped
// We'll still process Status and Health alerts
if containerStats == nil {
for _, alertRecord := range alerts {
name := alertRecord.GetString("name")
if name == "Status" || name == "Health" {
// Container is missing, so it's likely stopped
go am.sendContainerStatusAlert(alertRecord, systemRecord, containerId, "stopped")
}
}
continue
}
// Process each alert for this container
var validAlerts []ContainerAlertData
now := time.Now().UTC()
oldestTime := now
for _, alertRecord := range alerts {
name := alertRecord.GetString("name")
var val float64
unit := "%"
switch name {
case "CPU":
val = containerStats.Cpu
case "Memory":
val = containerStats.Mem
case "Network":
val = containerStats.NetworkSent + containerStats.NetworkRecv
unit = " MB/s"
case "Status":
// Status alert is handled differently - immediate trigger
triggered := alertRecord.GetBool("triggered")
isRunning := containerStats.Status == "running"
if (!triggered && !isRunning) || (triggered && isRunning) {
go am.sendContainerStatusAlert(alertRecord, systemRecord, containerId, containerStats.Status)
}
continue
case "Health":
// Health alert - trigger on unhealthy status
triggered := alertRecord.GetBool("triggered")
isHealthy := containerStats.Health == container.DockerHealthNone || containerStats.Health == container.DockerHealthHealthy
if (!triggered && !isHealthy) || (triggered && isHealthy) {
go am.sendContainerHealthAlert(alertRecord, systemRecord, containerId, containerStats.Health)
}
continue
default:
continue
}
triggered := alertRecord.GetBool("triggered")
threshold := alertRecord.GetFloat("value")
min := max(1, cast.ToUint8(alertRecord.Get("min")))
alert := ContainerAlertData{
systemRecord: systemRecord,
alertRecord: alertRecord,
name: name,
unit: unit,
val: val,
threshold: threshold,
triggered: triggered,
min: min,
}
// Send alert immediately if min is 1
if min == 1 {
alert.triggered = val > threshold
go am.sendContainerAlert(alert, containerId, containerStats.Name)
continue
}
alert.time = now.Add(-time.Duration(min) * time.Minute)
if alert.time.Before(oldestTime) {
oldestTime = alert.time
}
validAlerts = append(validAlerts, alert)
}
if len(validAlerts) == 0 {
continue
}
// Fetch historical container stats
containerStatsRecords := []struct {
Stats []byte `db:"stats"`
Created types.DateTime `db:"created"`
}{}
err = am.hub.DB().
Select("stats", "created").
From("container_stats").
Where(dbx.NewExp(
"system={:system} AND type='1m' AND created > {:created}",
dbx.Params{
"system": systemRecord.Id,
"created": oldestTime.Add(-time.Second * 90),
},
)).
OrderBy("created").
All(&containerStatsRecords)
if err != nil || len(containerStatsRecords) == 0 {
continue
}
oldestRecordTime := containerStatsRecords[0].Created.Time()
// Filter valid alerts
filteredAlerts := make([]ContainerAlertData, 0, len(validAlerts))
for _, alert := range validAlerts {
if alert.time.After(oldestRecordTime) {
filteredAlerts = append(filteredAlerts, alert)
}
}
validAlerts = filteredAlerts
if len(validAlerts) == 0 {
continue
}
// Process historical stats
for i := range containerStatsRecords {
stat := containerStatsRecords[i]
systemStatsCreation := stat.Created.Time().Add(-time.Second * 10)
// Parse container stats array
var allContainerStats []*container.Stats
if err := json.Unmarshal(stat.Stats, &allContainerStats); err != nil {
continue
}
// Find this container in the stats
var thisContainerStats *container.Stats
for _, cs := range allContainerStats {
if cs.Id == containerId {
thisContainerStats = cs
break
}
}
if thisContainerStats == nil {
continue
}
for j := range validAlerts {
alert := &validAlerts[j]
if i == 0 {
alert.val = 0
}
if systemStatsCreation.Before(alert.time) {
continue
}
switch alert.name {
case "CPU":
alert.val += thisContainerStats.Cpu
case "Memory":
alert.val += thisContainerStats.Mem
case "Network":
alert.val += thisContainerStats.NetworkSent + thisContainerStats.NetworkRecv
default:
continue
}
alert.count++
}
}
// Calculate averages and send alerts
for _, alert := range validAlerts {
alert.val = alert.val / float64(alert.count)
minCount := float32(alert.min) / 1.2
if float32(alert.count) >= minCount {
if !alert.triggered && alert.val > alert.threshold {
alert.triggered = true
go am.sendContainerAlert(alert, containerId, containerStats.Name)
} else if alert.triggered && alert.val <= alert.threshold {
alert.triggered = false
go am.sendContainerAlert(alert, containerId, containerStats.Name)
}
}
}
}
return nil
}
// sendContainerAlert sends a container alert notification
func (am *AlertManager) sendContainerAlert(alert ContainerAlertData, containerId, containerName string) {
systemName := alert.systemRecord.GetString("name")
var subject string
if alert.triggered {
subject = fmt.Sprintf("%s container %s %s above threshold", systemName, containerName, strings.ToLower(alert.name))
} else {
subject = fmt.Sprintf("%s container %s %s below threshold", systemName, containerName, strings.ToLower(alert.name))
}
minutesLabel := "minute"
if alert.min > 1 {
minutesLabel += "s"
}
descriptor := alert.name
if alert.descriptor != "" {
descriptor = alert.descriptor
}
body := fmt.Sprintf("%s averaged %.2f%s for the previous %v %s.", descriptor, alert.val, alert.unit, alert.min, minutesLabel)
alert.alertRecord.Set("triggered", alert.triggered)
if err := am.hub.Save(alert.alertRecord); err != nil {
return
}
am.SendAlert(AlertMessageData{
UserID: alert.alertRecord.GetString("user"),
SystemID: alert.systemRecord.Id,
Title: subject,
Message: body,
Link: am.hub.MakeLink("system", alert.systemRecord.Id, "containers", containerId),
LinkText: "View " + containerName,
})
}
// sendContainerStatusAlert sends a container status change alert
func (am *AlertManager) sendContainerStatusAlert(alertRecord, systemRecord *core.Record, containerId, status string) {
triggered := alertRecord.GetBool("triggered")
isRunning := status == "running"
// Only send if state changed
if (!triggered && isRunning) || (triggered && !isRunning) {
return
}
systemName := systemRecord.GetString("name")
// Try to get container name from containers collection
containerName := containerId
if containerRecord, err := am.hub.FindFirstRecordByFilter("containers", "id={:id}", dbx.Params{"id": containerId}); err == nil {
containerName = containerRecord.GetString("name")
}
var subject string
if isRunning {
subject = fmt.Sprintf("%s container %s is now running", systemName, containerName)
} else {
subject = fmt.Sprintf("%s container %s has stopped", systemName, containerName)
}
body := fmt.Sprintf("Container status changed to: %s", status)
alertRecord.Set("triggered", !isRunning)
if err := am.hub.Save(alertRecord); err != nil {
return
}
am.SendAlert(AlertMessageData{
UserID: alertRecord.GetString("user"),
SystemID: systemRecord.Id,
Title: subject,
Message: body,
Link: am.hub.MakeLink("system", systemRecord.Id, "containers", containerId),
LinkText: "View " + containerName,
})
}
// sendContainerHealthAlert sends a container health status change alert
func (am *AlertManager) sendContainerHealthAlert(alertRecord, systemRecord *core.Record, containerId string, health container.DockerHealth) {
triggered := alertRecord.GetBool("triggered")
isHealthy := health == container.DockerHealthNone || health == container.DockerHealthHealthy
// Only send if state changed
if (!triggered && isHealthy) || (triggered && !isHealthy) {
return
}
systemName := systemRecord.GetString("name")
// Try to get container name from containers collection
containerName := containerId
if containerRecord, err := am.hub.FindFirstRecordByFilter("containers", "id={:id}", dbx.Params{"id": containerId}); err == nil {
containerName = containerRecord.GetString("name")
}
var subject string
if isHealthy {
subject = fmt.Sprintf("%s container %s is now healthy", systemName, containerName)
} else {
subject = fmt.Sprintf("%s container %s is unhealthy", systemName, containerName)
}
// Convert DockerHealth to string
healthStatus := "unknown"
for str, val := range container.DockerHealthStrings {
if val == health {
healthStatus = str
break
}
}
body := fmt.Sprintf("Container health status changed to: %s", healthStatus)
alertRecord.Set("triggered", !isHealthy)
if err := am.hub.Save(alertRecord); err != nil {
return
}
am.SendAlert(AlertMessageData{
UserID: alertRecord.GetString("user"),
SystemID: systemRecord.Id,
Title: subject,
Message: body,
Link: am.hub.MakeLink("system", systemRecord.Id, "containers", containerId),
LinkText: "View " + containerName,
})
}
+67
View File
@@ -72,3 +72,70 @@ func createAlertHistoryRecord(app core.App, alertRecord *core.Record) (alertHist
}
return alertHistoryRecord, err
}
// On triggered container alert record delete, set matching alert history record to resolved
func resolveHistoryOnContainerAlertDelete(e *core.RecordEvent) error {
if !e.Record.GetBool("triggered") {
return e.Next()
}
_ = resolveContainerAlertHistoryRecord(e.App, e.Record.Id)
return e.Next()
}
// On container alert record update, update alert history record
func updateHistoryOnContainerAlertUpdate(e *core.RecordEvent) error {
original := e.Record.Original()
new := e.Record
originalTriggered := original.GetBool("triggered")
newTriggered := new.GetBool("triggered")
// no need to update alert history if triggered state has not changed
if originalTriggered == newTriggered {
return e.Next()
}
// if new state is triggered, create new alert history record
if newTriggered {
_, _ = createContainerAlertHistoryRecord(e.App, new)
return e.Next()
}
// if new state is not triggered, check for matching alert history record and set it to resolved
_ = resolveContainerAlertHistoryRecord(e.App, new.Id)
return e.Next()
}
// resolveContainerAlertHistoryRecord sets the resolved field to the current time
func resolveContainerAlertHistoryRecord(app core.App, alertRecordID string) error {
alertHistoryRecord, err := app.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id} && resolved=null", dbx.Params{"alert_id": alertRecordID})
if err != nil || alertHistoryRecord == nil {
return err
}
alertHistoryRecord.Set("resolved", time.Now().UTC())
err = app.Save(alertHistoryRecord)
if err != nil {
app.Logger().Error("Failed to resolve container alert history", "err", err)
}
return err
}
// createContainerAlertHistoryRecord creates a new container alert history record
func createContainerAlertHistoryRecord(app core.App, alertRecord *core.Record) (alertHistoryRecord *core.Record, err error) {
alertHistoryCollection, err := app.FindCachedCollectionByNameOrId("alerts_history")
if err != nil {
return nil, err
}
alertHistoryRecord = core.NewRecord(alertHistoryCollection)
alertHistoryRecord.Set("alert_id", alertRecord.Id)
alertHistoryRecord.Set("user", alertRecord.GetString("user"))
alertHistoryRecord.Set("system", alertRecord.GetString("system"))
alertHistoryRecord.Set("container", alertRecord.GetString("container"))
alertHistoryRecord.Set("name", alertRecord.GetString("name"))
alertHistoryRecord.Set("value", alertRecord.GetFloat("value"))
err = app.Save(alertHistoryRecord)
if err != nil {
app.Logger().Error("Failed to save container alert history", "err", err)
}
return alertHistoryRecord, err
}
+6 -42
View File
@@ -31,6 +31,9 @@ func (opts *cmdOptions) parse() bool {
// Subcommands that don't require any pflag parsing
switch subcommand {
case "-v", "version":
fmt.Println(beszel.AppName+"-agent", beszel.Version)
return true
case "health":
err := health.Check()
if err != nil {
@@ -38,9 +41,6 @@ func (opts *cmdOptions) parse() bool {
}
fmt.Print("ok")
return true
case "fingerprint":
handleFingerprint()
return true
}
// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
@@ -49,7 +49,6 @@ func (opts *cmdOptions) parse() bool {
pflag.StringVarP(&opts.hubURL, "url", "u", "", "URL of the Beszel hub")
pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
version := pflag.BoolP("version", "v", false, "Show version information")
help := pflag.BoolP("help", "h", false, "Show this help message")
// Convert old single-dash long flags to double-dash for backward compatibility
@@ -74,9 +73,9 @@ func (opts *cmdOptions) parse() bool {
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" fingerprint View or reset the agent fingerprint\n")
builder.WriteString(" health Check if the agent is running\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString(" health Check if the agent is running\n")
// builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
pflag.PrintDefaults()
@@ -87,9 +86,6 @@ func (opts *cmdOptions) parse() bool {
// Must run after pflag.Parse()
switch {
case *version:
fmt.Println(beszel.AppName+"-agent", beszel.Version)
return true
case *help || subcommand == "help":
pflag.Usage()
return true
@@ -137,38 +133,6 @@ func (opts *cmdOptions) getAddress() string {
return agent.GetAddress(opts.listen)
}
// handleFingerprint handles the "fingerprint" command with subcommands "view" and "reset".
func handleFingerprint() {
subCmd := ""
if len(os.Args) > 2 {
subCmd = os.Args[2]
}
switch subCmd {
case "", "view":
dataDir, _ := agent.GetDataDir()
fp := agent.GetFingerprint(dataDir, "", "")
fmt.Println(fp)
case "help", "-h", "--help":
fmt.Print(fingerprintUsage())
case "reset":
dataDir, err := agent.GetDataDir()
if err != nil {
log.Fatal(err)
}
if err := agent.DeleteFingerprint(dataDir); err != nil {
log.Fatal(err)
}
fmt.Println("Fingerprint reset. A new one will be generated on next start.")
default:
log.Fatalf("Unknown command: %q\n\n%s", subCmd, fingerprintUsage())
}
}
func fingerprintUsage() string {
return fmt.Sprintf("Usage: %s fingerprint [view|reset]\n\nCommands:\n view Print fingerprint (default)\n reset Reset saved fingerprint\n", os.Args[0])
}
func main() {
var opts cmdOptions
subcommandHandled := opts.parse()
+3
View File
@@ -296,6 +296,9 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
// update / delete user alerts
apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts)
apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts)
// update / delete container alerts
apiAuth.POST("/user-container-alerts", alerts.UpsertUserContainerAlerts)
apiAuth.DELETE("/user-container-alerts", alerts.DeleteUserContainerAlerts)
// refresh SMART devices for a system
apiAuth.POST("/smart/refresh", h.refreshSmartData)
// get systemd service details
+5
View File
@@ -51,6 +51,7 @@ type hubLike interface {
core.App
GetSSHKey(dataDir string) (ssh.Signer, error)
HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error
HandleContainerAlerts(systemRecord *core.Record, data *system.CombinedData) error
HandleStatusAlerts(status string, systemRecord *core.Record) error
}
@@ -211,6 +212,10 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
if err := sm.hub.HandleSystemAlerts(e.Record, system.data); err != nil {
e.App.Logger().Error("Error handling system alerts", "err", err)
}
// Trigger container alerts
if err := sm.hub.HandleContainerAlerts(e.Record, system.data); err != nil {
e.App.Logger().Error("Error handling container alerts", "err", err)
}
}
// Trigger status change alerts for up/down transitions
@@ -0,0 +1,208 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// 1. Create container_alerts collection
// Check if collection already exists to prevent errors on restart
if _, err := app.FindCollectionByNameOrId("container_alerts"); err == nil {
// Collection exists, skip creation
} else {
jsonData := `{
"id": "ca_container_alerts",
"name": "container_alerts",
"type": "base",
"system": false,
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"viewRule": "",
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"fields": [
{
"id": "text_id",
"name": "id",
"type": "text",
"required": true,
"presentable": false,
"unique": false,
"system": true,
"primaryKey": true,
"autogeneratePattern": "[a-z0-9]{15}",
"min": 15,
"max": 15,
"pattern": "^[a-z0-9]+$"
},
{
"id": "rel_user",
"name": "user",
"type": "relation",
"required": true,
"presentable": false,
"unique": false,
"system": false,
"collectionId": "_pb_users_auth_",
"cascadeDelete": true,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
},
{
"id": "rel_system",
"name": "system",
"type": "relation",
"required": true,
"presentable": false,
"unique": false,
"system": false,
"collectionId": "2hz5ncl8tizk5nx",
"cascadeDelete": true,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
},
{
"id": "text_container",
"name": "container",
"type": "text",
"required": true,
"presentable": false,
"unique": false,
"system": false,
"min": null,
"max": null,
"pattern": ""
},
{
"id": "select_name",
"name": "name",
"type": "select",
"required": true,
"presentable": false,
"unique": false,
"system": false,
"maxSelect": 1,
"values": [
"Status",
"CPU",
"Memory",
"Network",
"Health"
]
},
{
"id": "number_value",
"name": "value",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"system": false,
"min": null,
"max": null,
"onlyInt": false
},
{
"id": "number_min",
"name": "min",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"system": false,
"min": null,
"max": 60,
"onlyInt": true
},
{
"id": "bool_triggered",
"name": "triggered",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"system": false
},
{
"id": "autodate_created",
"name": "created",
"type": "autodate",
"required": false,
"presentable": false,
"unique": false,
"system": false,
"onCreate": true,
"onUpdate": false
},
{
"id": "autodate_updated",
"name": "updated",
"type": "autodate",
"required": false,
"presentable": false,
"unique": false,
"system": false,
"onCreate": true,
"onUpdate": true
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_container_alerts_unique` + "`" + ` ON ` + "`" + `container_alerts` + "`" + ` (` + "`" + `user` + "`" + `, ` + "`" + `system` + "`" + `, ` + "`" + `container` + "`" + `, ` + "`" + `name` + "`" + `)"
]
}`
collection := &core.Collection{}
if err := json.Unmarshal([]byte(jsonData), collection); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
}
// 2. Add container field to alerts_history collection
alertsHistoryCollection, err := app.FindCollectionByNameOrId("alerts_history")
if err != nil {
return err
}
if alertsHistoryCollection.Fields.GetByName("container") == nil {
containerField := &core.TextField{
Name: "container",
Required: false,
}
alertsHistoryCollection.Fields.Add(containerField)
return app.Save(alertsHistoryCollection)
}
return nil
}, func(app core.App) error {
// Rollback 2: remove the container field
alertsHistoryCollection, err := app.FindCollectionByNameOrId("alerts_history")
if err == nil {
if field := alertsHistoryCollection.Fields.GetByName("container"); field != nil {
alertsHistoryCollection.Fields.RemoveByName("container")
if err := app.Save(alertsHistoryCollection); err != nil {
return err
}
}
}
// Rollback 1: delete the container_alerts collection
collection, err := app.FindCollectionByNameOrId("container_alerts")
if err == nil {
return app.Delete(collection)
}
return nil
})
}
+2 -6
View File
@@ -190,8 +190,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
id := record.Id
// clear global statsRecord for reuse
statsRecord.Stats = statsRecord.Stats[:0]
// reset tempStats each iteration to avoid omitzero fields retaining stale values
*stats = system.Stats{}
queryParams["id"] = id
db.NewQuery("SELECT stats FROM system_stats WHERE id = {:id}").Bind(queryParams).One(&statsRecord)
@@ -446,11 +444,9 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
for i := range records {
id := records[i].Id
// clear global statsRecord for reuse
// clear global statsRecord and containerStats for reuse
statsRecord.Stats = statsRecord.Stats[:0]
// must set to nil (not [:0]) to avoid json.Unmarshal reusing backing array
// which causes omitzero fields to inherit stale values from previous iterations
containerStats = nil
containerStats = containerStats[:0]
queryParams["id"] = id
db.NewQuery("SELECT stats FROM container_stats WHERE id = {:id}").Bind(queryParams).One(&statsRecord)
+44 -14
View File
@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.18.3",
"version": "0.18.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.18.3",
"version": "0.18.2",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -111,6 +111,7 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1137,6 +1138,7 @@
"integrity": "sha512-9IO+PDvdneY8OCI8zvI1oDXpzryTMtyRv7uq9O0U1mFCvIPVd5dWQKQDu/CpgpYAc2+JG/izn5PNl9xzPc6ckw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.20.12",
"@babel/runtime": "^7.20.13",
@@ -1290,6 +1292,7 @@
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-5.4.1.tgz",
"integrity": "sha512-4FeIh56PH5vziPg2BYo4XYWWOHE4XaY/XR8Jakwn0/qwtLpydWMNVpZOpGWi7nfPZtcLaJLmZKup6UNxEl1Pfw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/message-utils": "5.4.1"
@@ -3485,6 +3488,7 @@
"integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3495,6 +3499,7 @@
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3699,6 +3704,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -5072,9 +5078,9 @@
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.sortby": {
@@ -5316,9 +5322,9 @@
}
},
"node_modules/minizlib": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5328,6 +5334,22 @@
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -5371,6 +5393,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
@@ -5580,6 +5603,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5725,6 +5749,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz",
"integrity": "sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5734,6 +5759,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.2.tgz",
"integrity": "sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -6273,7 +6299,8 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tapable": {
"version": "2.2.3",
@@ -6290,16 +6317,17 @@
}
},
"node_modules/tar": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"dev": true,
"license": "BlueOak-1.0.0",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.1.0",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
@@ -6394,6 +6422,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6628,6 +6657,7 @@
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.18.3",
"version": "0.18.2",
"type": "module",
"scripts": {
"dev": "vite --host",
@@ -0,0 +1,44 @@
import { t } from "@lingui/core/macro"
import { useStore } from "@nanostores/react"
import { BellIcon } from "lucide-react"
import { memo, useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { $containerAlerts } from "@/lib/stores"
import { cn } from "@/lib/utils"
import type { ContainerRecord } from "@/types"
import { ContainerAlertDialogContent } from "./container-alerts-sheet"
export default memo(function ContainerAlertButton({
systemId,
container,
}: {
systemId: string
container: ContainerRecord
}) {
const [opened, setOpened] = useState(false)
const alerts = useStore($containerAlerts)
const containerAlerts = alerts[systemId]?.get(container.id)
const hasContainerAlert = containerAlerts && containerAlerts.size > 0
return useMemo(
() => (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
<BellIcon
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
"fill-primary": hasContainerAlert,
})}
/>
</Button>
</SheetTrigger>
<SheetContent className="max-h-full overflow-auto w-150 !max-w-full p-4 sm:p-6">
{opened && <ContainerAlertDialogContent systemId={systemId} container={container} />}
</SheetContent>
</Sheet>
),
[opened, hasContainerAlert]
)
})
@@ -0,0 +1,327 @@
import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { BoxIcon, GlobeIcon } from "lucide-react"
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 { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { toast } from "@/components/ui/use-toast"
import { containerAlertInfo } from "@/lib/container-alerts"
import { pb } from "@/lib/api"
import { $containerAlerts } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils"
import type { ContainerAlertInfo, ContainerAlertRecord, ContainerRecord } from "@/types"
const Slider = lazy(() => import("@/components/ui/slider"))
const endpoint = "/api/beszel/user-container-alerts"
const alertDebounce = 100
const alertKeys = Object.keys(containerAlertInfo) as (keyof typeof containerAlertInfo)[]
const failedUpdateToast = (error: unknown) => {
console.error(error)
toast({
title: t`Failed to update alert`,
description: t`Please check logs for more details.`,
variant: "destructive",
})
}
/** Create or update container alerts */
const upsertContainerAlerts = debounce(
async ({
name,
value,
min,
systems,
containers,
}: {
name: string
value: number
min: number
systems: string[]
containers: string[]
}) => {
try {
await pb.send<{ success: boolean }>(endpoint, {
method: "POST",
body: { name, value, min, systems, containers, overwrite: true },
})
} catch (error) {
failedUpdateToast(error)
}
},
alertDebounce
)
/** Delete container alerts */
const deleteContainerAlerts = debounce(
async ({ name, systems, containers }: { name: string; systems: string[]; containers: string[] }) => {
try {
await pb.send<{ success: boolean }>(endpoint, {
method: "DELETE",
body: { name, systems, containers },
})
} catch (error) {
failedUpdateToast(error)
}
},
alertDebounce
)
export const ContainerAlertDialogContent = memo(function ContainerAlertDialogContent({
systemId,
container,
}: {
systemId: string
container: ContainerRecord
}) {
const alerts = useStore($containerAlerts)
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
const [currentTab, setCurrentTab] = useState("container")
const containerAlerts = alerts[systemId]?.get(container.id) ?? new Map()
// Keep a copy of alerts when we switch to global tab
const alertsWhenGlobalSelected = useMemo(() => {
return currentTab === "global" ? structuredClone(alerts) : alerts
}, [currentTab])
return (
<>
<DialogHeader>
<DialogTitle className="text-xl">
<Trans>Container Alerts</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
See{" "}
<Link href={getPagePath($router, "settings", { name: "notifications" })} className="link">
notification settings
</Link>{" "}
to configure how you receive alerts.
</Trans>
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="container" onValueChange={setCurrentTab}>
<TabsList className="mb-1 -mt-0.5">
<TabsTrigger value="container">
<BoxIcon className="me-2 h-3.5 w-3.5" />
<span className="truncate max-w-60">{container.name}</span>
</TabsTrigger>
<TabsTrigger value="global">
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
<Trans>All Containers</Trans>
</TabsTrigger>
</TabsList>
<TabsContent value="container">
<div className="grid gap-3">
{alertKeys.map((name) => (
<ContainerAlertContent
key={name}
alertKey={name}
data={containerAlertInfo[name as keyof typeof containerAlertInfo]}
alert={containerAlerts.get(name)}
systemId={systemId}
container={container}
/>
))}
</div>
</TabsContent>
<TabsContent value="global">
<label
htmlFor="ovw"
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
>
<Checkbox
id="ovw"
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
checked={overwriteExisting}
onCheckedChange={setOverwriteExisting}
/>
<Trans>Overwrite existing alerts</Trans>
</label>
<div className="grid gap-3">
{alertKeys.map((name) => (
<ContainerAlertContent
key={name}
alertKey={name}
systemId={systemId}
container={container}
alert={containerAlerts.get(name)}
data={containerAlertInfo[name as keyof typeof containerAlertInfo]}
global={true}
overwriteExisting={!!overwriteExisting}
initialAlertsState={alertsWhenGlobalSelected}
/>
))}
</div>
</TabsContent>
</Tabs>
</>
)
})
export function ContainerAlertContent({
alertKey,
data: alertData,
systemId,
container,
alert,
global = false,
overwriteExisting = false,
initialAlertsState = {},
}: {
alertKey: string
data: ContainerAlertInfo
systemId: string
container: ContainerRecord
alert?: ContainerAlertRecord
global?: boolean
overwriteExisting?: boolean
initialAlertsState?: Record<string, Map<string, Map<string, ContainerAlertRecord>>>
}) {
const { name } = alertData
const singleDescription = alertData.singleDesc?.()
const [checked, setChecked] = useState(global ? false : !!alert)
const [min, setMin] = useState(alert?.min || 10)
const [value, setValue] = useState(alert?.value || (singleDescription ? 0 : alertData.start ?? 80))
const Icon = alertData.icon
/** Get container ids to update */
function getContainerIds(): string[] {
// if not global, update only the current container
if (!global) {
return [container.id]
}
// if global, we need to get all containers for this system
// For now, we'll just use the current container
// In a real implementation, you'd fetch all containers for the system
return [container.id]
}
function sendUpsert(min: number, value: number) {
const containers = getContainerIds()
containers.length &&
upsertContainerAlerts({
name: alertKey,
value,
min,
systems: [systemId],
containers,
})
}
return (
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
<label
htmlFor={`c${name}`}
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
"pb-0": checked,
})}
>
<div className="grid gap-1 select-none">
<p className="font-semibold flex gap-3 items-center">
<Icon className="h-4 w-4 opacity-85" /> {alertData.name()}
</p>
{!checked && <span className="block text-sm text-muted-foreground">{alertData.desc()}</span>}
</div>
<Switch
id={`c${name}`}
checked={checked}
onCheckedChange={(newChecked) => {
setChecked(newChecked)
if (newChecked) {
// if alert checked, create or update alert
sendUpsert(min, value)
} else {
// if unchecked, delete alert
deleteContainerAlerts({ name: alertKey, systems: [systemId], containers: getContainerIds() })
// when force deleting all alerts of a type, also remove them from initialAlertsState
if (overwriteExisting) {
for (const systemAlerts of Object.values(initialAlertsState)) {
for (const containerAlerts of systemAlerts.values()) {
containerAlerts.delete(alertKey)
}
}
}
}
}}
/>
</label>
{checked && (
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
<Suspense fallback={<div className="h-10" />}>
{!singleDescription && (
<div>
<p id={`v${name}`} className="text-sm block h-8">
{alertData.invert ? (
<Trans>
Average drops below{" "}
<strong className="text-foreground">
{value}
{alertData.unit}
</strong>
</Trans>
) : (
<Trans>
Average exceeds{" "}
<strong className="text-foreground">
{value}
{alertData.unit}
</strong>
</Trans>
)}
</p>
<div className="flex gap-3">
<Slider
aria-labelledby={`v${name}`}
defaultValue={[value]}
onValueCommit={(val) => sendUpsert(min, val[0])}
onValueChange={(val) => setValue(val[0])}
step={alertData.step ?? 1}
min={alertData.min ?? 1}
max={alertData.max ?? 99}
/>
</div>
</div>
)}
<div className={cn(singleDescription && "col-span-full lowercase")}>
<p id={`t${name}`} className="text-sm block h-8 first-letter:uppercase">
{singleDescription && (
<>
{singleDescription}
{` `}
</>
)}
<Trans>
For <strong className="text-foreground">{min}</strong>{" "}
<Plural value={min} one="minute" other="minutes" />
</Trans>
</p>
<div className="flex gap-3">
<Slider
aria-labelledby={`v${name}`}
defaultValue={[min]}
onValueCommit={(minVal) => sendUpsert(minVal[0], value)}
onValueChange={(val) => setMin(val[0])}
min={1}
max={60}
/>
</div>
</div>
</Suspense>
</div>
)}
</div>
)
}
@@ -38,23 +38,6 @@ export default memo(function ContainerChart({
const isNetChart = chartType === ChartType.Network
// Filter with set lookup
const filteredKeys = useMemo(() => {
if (!filter) {
return new Set<string>()
}
const filterTerms = filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
return new Set(
Object.keys(chartConfig).filter((key) => {
const keyLower = key.toLowerCase()
return !filterTerms.some((term) => keyLower.includes(term))
})
)
}, [chartConfig, filter])
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
const obj = {} as {
@@ -102,14 +85,10 @@ export default memo(function ContainerChart({
let totalSent = 0
let totalRecv = 0
const payloadData = item?.payload && typeof item.payload === "object" ? item.payload : {}
for (const [containerKey, value] of Object.entries(payloadData)) {
for (const value of Object.values(payloadData)) {
if (!value || typeof value !== "object") {
continue
}
// Skip filtered out containers
if (filteredKeys.has(containerKey)) {
continue
}
const [sent, recv] = getRxTxBytes(value as { b?: [number, number]; ns?: number; nr?: number })
totalSent += sent
totalRecv += recv
@@ -145,7 +124,24 @@ export default memo(function ContainerChart({
obj.dataFunction = (key: string, data: any) => data[key]?.[dataKey] ?? null
}
return obj
}, [filteredKeys])
}, [])
// Filter with set lookup
const filteredKeys = useMemo(() => {
if (!filter) {
return new Set<string>()
}
const filterTerms = filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
return new Set(
Object.keys(chartConfig).filter((key) => {
const keyLower = key.toLowerCase()
return !filterTerms.some((term) => keyLower.includes(term))
})
)
}, [chartConfig, filter])
// console.log('rendered at', new Date())
@@ -1,4 +1,5 @@
import type { Column, ColumnDef } from "@tanstack/react-table"
import { lazy, Suspense } from "react"
import { Button } from "@/components/ui/button"
import { cn, decimalString, formatBytes, hourWithSeconds } from "@/lib/utils"
import type { ContainerRecord } from "@/types"
@@ -31,8 +32,7 @@ const unitSeconds = [
// Convert docker status string to number of seconds ("Up X minutes", "Up X hours", etc.)
function getStatusValue(status: string): number {
const [_, num, unit] = status.split(" ")
// Docker uses "a" or "an" instead of "1" for singular units (e.g., "Up a minute", "Up an hour")
const numValue = num === "a" || num === "an" ? 1 : Number(num)
const numValue = Number(num)
for (const [unitName, value] of unitSeconds) {
if (unit.startsWith(unitName)) {
return numValue * value
@@ -41,6 +41,9 @@ function getStatusValue(status: string): number {
return 0
}
// Lazy load the alert button component
const ContainerAlertButton = lazy(() => import("@/components/alerts/container-alert-button"))
export const containerChartCols: ColumnDef<ContainerRecord>[] = [
{
id: "name",
@@ -63,7 +66,7 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
cell: ({ getValue }) => {
const allSystems = useStore($allSystemsById)
return <span className="ms-1.5 xl:w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
return <span className="ms-1.5 xl:w-30 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
},
},
// {
@@ -138,17 +141,17 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
id: "image",
sortingFn: (a, b) => a.original.image.localeCompare(b.original.image),
accessorFn: (record) => record.image,
header: ({ column }) => (
<HeaderButton column={column} name={t({ message: "Image", context: "Docker image" })} Icon={LayersIcon} />
),
enableHiding: true,
header: ({ column }) => <HeaderButton column={column} name={t({ message: "Image", context: "Docker image" })} Icon={LayersIcon} />,
cell: ({ getValue }) => {
return <span className="ms-1.5 xl:w-40 block truncate">{getValue() as string}</span>
return <span className="ms-1.5 xl:w-30 block truncate">{getValue() as string}</span>
},
},
{
id: "status",
accessorFn: (record) => record.status,
invertSorting: true,
enableHiding: true,
sortingFn: (a, b) => getStatusValue(a.original.status) - getStatusValue(b.original.status),
header: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={HourglassIcon} />,
cell: ({ getValue }) => {
@@ -165,6 +168,21 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
return <span className="ms-1.5 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
},
},
{
id: "actions",
header: () => <div className="text-center">{t`Actions`}</div>,
cell: ({ row }) => {
// Lazy load the alert button component
const ContainerAlertButton = lazy(() => import("@/components/alerts/container-alert-button"))
return (
<div className="flex justify-center" onClick={(e) => e.stopPropagation()}>
<Suspense fallback={<div className="h-8 w-8" />}>
<ContainerAlertButton systemId={row.original.system} container={row.original} />
</Suspense>
</div>
)
},
},
]
function HeaderButton({
@@ -231,7 +231,7 @@ const AllContainersTable = memo(function AllContainersTable({
>
{/* 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">
<table className="text-sm w-full h-full">
<ContainersTableHead table={table} />
<TableBody>
{rows.length ? (
+3 -1
View File
@@ -75,7 +75,9 @@ export default function Navbar() {
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
</Link>
</TooltipTrigger>
<TooltipContent>S.M.A.R.T.</TooltipContent>
<TooltipContent>
<Trans>S.M.A.R.T.</Trans>
</TooltipContent>
</Tooltip>
<LangToggle />
<ModeToggle />
@@ -363,8 +363,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
e.target instanceof HTMLTextAreaElement ||
e.shiftKey ||
e.ctrlKey ||
e.metaKey ||
e.altKey
e.metaKey
) {
return
}
@@ -8,7 +8,6 @@ import type { ClassValue } from "clsx"
import {
ArrowUpDownIcon,
ChevronRightSquareIcon,
ClockArrowUp,
CopyIcon,
CpuIcon,
HardDriveIcon,
@@ -35,7 +34,6 @@ import {
formatTemperature,
getMeterState,
parseSemVer,
secondsToString,
} from "@/lib/utils"
import { batteryStateTranslations } from "@/lib/i18n"
import type { SystemRecord } from "@/types"
@@ -375,29 +373,6 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
)
},
},
{
accessorFn: ({ info }) => info.u || undefined,
id: "uptime",
name: () => t`Uptime`,
size: 50,
Icon: ClockArrowUp,
header: sortableHeader,
cell(info) {
const uptime = info.getValue() as number
if (!uptime) {
return null
}
let formatted: string
if (uptime < 3600) {
formatted = secondsToString(uptime, "minute")
} else if (uptime < 360000) {
formatted = secondsToString(uptime, "hour")
} else {
formatted = secondsToString(uptime, "day")
}
return <span className="tabular-nums whitespace-nowrap">{formatted}</span>
},
},
{
accessorFn: ({ info }) => info.v,
id: "agent",
+2 -1
View File
@@ -3,7 +3,7 @@ import PocketBase from "pocketbase"
import { basePath } from "@/components/router"
import { toast } from "@/components/ui/use-toast"
import type { ChartTimes, UserSettings } from "@/types"
import { $alerts, $allSystemsById, $allSystemsByName, $userSettings } from "./stores"
import { $alerts, $allSystemsById, $allSystemsByName, $containerAlerts, $userSettings } from "./stores"
import { chartTimeData } from "./utils"
/** PocketBase JS Client */
@@ -30,6 +30,7 @@ export function logOut() {
$allSystemsByName.set({})
$allSystemsById.set({})
$alerts.set({})
$containerAlerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
+180
View File
@@ -0,0 +1,180 @@
import { t } from "@lingui/core/macro"
import { CpuIcon, HeartPulseIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
import type { RecordSubscription } from "pocketbase"
import { EthernetIcon } from "@/components/ui/icons"
import { $containerAlerts } from "@/lib/stores"
import type { ContainerAlertInfo, ContainerAlertRecord } from "@/types"
import { pb } from "./api"
/** Container alert info for each alert type */
export const containerAlertInfo: Record<string, ContainerAlertInfo> = {
Status: {
name: () => t`Status`,
unit: "",
icon: ServerIcon,
desc: () => t`Triggers when container status changes`,
singleDesc: () => `${t`Container`} ${t`Stopped`}`,
},
CPU: {
name: () => t`CPU Usage`,
unit: "%",
icon: CpuIcon,
desc: () => t`Triggers when CPU usage exceeds a threshold`,
},
Memory: {
name: () => t`Memory Usage`,
unit: "%",
icon: MemoryStickIcon,
desc: () => t`Triggers when memory usage exceeds a threshold`,
},
Network: {
name: () => t`Network`,
unit: " MB/s",
icon: EthernetIcon,
desc: () => t`Triggers when combined up/down exceeds a threshold`,
max: 125,
},
Health: {
name: () => t`Health Status`,
unit: "",
icon: HeartPulseIcon,
desc: () => t`Triggers when container health status changes`,
singleDesc: () => `${t`Container`} ${t`Unhealthy`}`,
},
}
class ContainerAlertManager {
private unsubscribeFn?: () => void
/**
* Subscribe to container alert updates
*/
async subscribe() {
if (this.unsubscribeFn) {
return
}
// Fetch initial container alerts
try {
const alerts = await pb.collection("container_alerts").getFullList<ContainerAlertRecord>()
this.updateStore(alerts)
} catch (e) {
console.error("Failed to fetch container alerts:", e)
}
// Subscribe to real-time updates
this.unsubscribeFn = await pb
.collection("container_alerts")
.subscribe<ContainerAlertRecord>("*", this.handleAlertUpdate)
}
/**
* Unsubscribe from container alert updates
*/
unsubscribe() {
if (this.unsubscribeFn) {
this.unsubscribeFn()
this.unsubscribeFn = undefined
}
}
/**
* Handle real-time alert updates
*/
private handleAlertUpdate = (e: RecordSubscription<ContainerAlertRecord>) => {
const { action, record } = e
if (action === "delete") {
this.deleteFromStore(record)
} else {
this.updateStore([record])
}
}
/**
* Update store with alert records
*/
private updateStore(alerts: ContainerAlertRecord[]) {
const currentAlerts = $containerAlerts.get()
for (const alert of alerts) {
if (!currentAlerts[alert.system]) {
currentAlerts[alert.system] = new Map()
}
if (!currentAlerts[alert.system].get(alert.container)) {
currentAlerts[alert.system].set(alert.container, new Map())
}
currentAlerts[alert.system].get(alert.container)!.set(alert.name, alert)
}
$containerAlerts.set(currentAlerts)
}
/**
* Delete alert from store
*/
private deleteFromStore(alert: ContainerAlertRecord) {
const currentAlerts = $containerAlerts.get()
if (currentAlerts[alert.system]?.get(alert.container)?.has(alert.name)) {
currentAlerts[alert.system].get(alert.container)!.delete(alert.name)
// Clean up empty maps
if (currentAlerts[alert.system].get(alert.container)!.size === 0) {
currentAlerts[alert.system].delete(alert.container)
}
if (currentAlerts[alert.system].size === 0) {
delete currentAlerts[alert.system]
}
$containerAlerts.set(currentAlerts)
}
}
/**
* Create or update container alerts
*/
async upsert(
systems: string[],
containers: string[],
name: string,
value: number,
min: number,
overwrite = false
) {
return pb.send("/api/beszel/user-container-alerts", {
method: "POST",
body: {
systems,
containers,
name,
value,
min,
overwrite,
},
})
}
/**
* Delete container alerts
*/
async delete(systems: string[], containers: string[], name: string) {
return pb.send("/api/beszel/user-container-alerts", {
method: "DELETE",
body: {
systems,
containers,
name,
},
})
}
/**
* Clear all container alerts from store
*/
clear() {
$containerAlerts.set({})
}
}
export const containerAlertManager = new ContainerAlertManager()
+4 -1
View File
@@ -1,5 +1,5 @@
import { atom, computed, listenKeys, map, type ReadableAtom } from "nanostores"
import type { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
import type { AlertMap, ChartTimes, ContainerAlertMap, SystemRecord, UserSettings } from "@/types"
import { pb } from "./api"
import { Unit } from "./enums"
@@ -22,6 +22,9 @@ export const $systems: ReadableAtom<SystemRecord[]> = computed($allSystemsById,
/** Map of alert records by system id and alert name */
export const $alerts = map<AlertMap>({})
/** Map of container alert records by system id, container id, and alert name */
export const $containerAlerts = map<ContainerAlertMap>({})
/** SSH public key */
export const $publicKey = atom("")
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-25 19:15\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
@@ -157,7 +157,6 @@ msgstr "التنبيهات"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "جميع الحاويات"
@@ -838,7 +837,6 @@ msgstr "غير نشط"
msgid "Invalid email address."
msgstr "عنوان البريد الإشباكي غير صالح."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "اللغة"
@@ -1337,7 +1335,6 @@ msgstr "تعيين عتبات النسبة المئوية لألوان العد
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "إلى البريد الإشباكي"
msgid "Toggle grid"
msgstr "تبديل الشبكة"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "تبديل السمة"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:17\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Тревоги"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Всички контейнери"
@@ -231,7 +230,7 @@ msgstr "Bandwidth на мрежата"
#. 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
@@ -838,7 +837,6 @@ msgstr "Неактивен"
msgid "Invalid email address."
msgstr "Невалиден имейл адрес."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Език"
@@ -1337,7 +1335,6 @@ msgstr "Задайте процентни прагове за цветовете
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "До имейл(ите)"
msgid "Toggle grid"
msgstr "Превключване на мрежа"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Включи тема"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-05 20:24\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -157,7 +157,6 @@ msgstr "Výstrahy"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Všechny kontejnery"
@@ -231,7 +230,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
@@ -838,7 +837,6 @@ msgstr "Neaktivní"
msgid "Invalid email address."
msgstr "Neplatná e-mailová adresa."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Jazyk"
@@ -1337,7 +1335,6 @@ msgstr "Nastavte procentuální prahové hodnoty pro barvy měřičů."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "Na email(y)"
msgid "Toggle grid"
msgstr "Přepnout mřížku"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Přepnout motiv"
+10 -14
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-19 10:55\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Alarmer"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Alle containere"
@@ -199,7 +198,7 @@ msgstr "Gennemsnit falder under <0>{value}{0}</0>"
#. placeholder {0}: alertData.unit
#: src/components/alerts/alerts-sheet.tsx
msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Gennemsnittet overstiger <0>{value}{0}</0>"
msgstr "Gennemsnit overstiger <0>{value}{0}</0>"
#: src/components/routes/system.tsx
msgid "Average power consumption of GPUs"
@@ -231,7 +230,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
@@ -401,7 +400,7 @@ msgstr "Forbindelsen er nede"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
msgstr "Fortsæt"
msgstr "Forsæt"
#: src/lib/utils.ts
msgid "Copied to clipboard"
@@ -426,7 +425,7 @@ msgstr "Kopier miljø"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
msgstr "Kopier vært"
msgstr "Kopier host"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -627,7 +626,7 @@ msgstr "Rediger {foo}"
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Email"
msgstr "E-mail"
#: src/components/routes/settings/notifications.tsx
msgid "Email notifications"
@@ -645,11 +644,11 @@ msgstr "Sluttid"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Indtast emailadresse for at nulstille adgangskoden"
msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
#: src/components/routes/settings/notifications.tsx
msgid "Enter email address..."
msgstr "Indtast emailadresse..."
msgstr "Indtast e-mailadresse..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
@@ -792,7 +791,7 @@ msgstr "GPU-enheder"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "GPU Strøm Træk"
msgstr "Gpu Strøm Træk"
#: src/lib/alerts.ts
msgid "GPU Usage"
@@ -838,7 +837,6 @@ msgstr "Inaktiv"
msgid "Invalid email address."
msgstr "Ugyldig email adresse."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Sprog"
@@ -1121,7 +1119,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"
@@ -1337,7 +1335,6 @@ msgstr "Indstil procentvise tærskler for målerfarver."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "Til email(s)"
msgid "Toggle grid"
msgstr "Slå gitter til/fra"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Skift tema"
+8 -12
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-05 20:24\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Warnungen"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Alle Container"
@@ -231,7 +230,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
@@ -599,11 +598,11 @@ msgstr "Dokumentation"
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Inaktiv"
msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Inaktiv ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
@@ -838,7 +837,6 @@ msgstr "Inaktiv"
msgid "Invalid email address."
msgstr "Ungültige E-Mail-Adresse."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Sprache"
@@ -971,7 +969,7 @@ msgstr "Name"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Net"
msgstr "Netzwerk"
msgstr "Netz"
#: src/components/routes/system.tsx
msgid "Network traffic of docker containers"
@@ -1121,7 +1119,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"
@@ -1337,7 +1335,6 @@ msgstr "Prozentuale Schwellenwerte für Zählerfarben festlegen."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "An E-Mail(s)"
msgid "Toggle grid"
msgstr "Raster umschalten"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Darstellung umschalten"
@@ -1617,11 +1613,11 @@ msgstr "Unbegrenzt"
#: src/components/routes/system/info-bar.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Up"
msgstr "Aktiv"
msgstr "aktiv"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Aktiv ({upSystemsLength})"
msgstr "aktiv ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
-4
View File
@@ -152,7 +152,6 @@ msgstr "Alerts"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "All Containers"
@@ -833,7 +832,6 @@ msgstr "Inactive"
msgid "Invalid email address."
msgstr "Invalid email address."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Language"
@@ -1332,7 +1330,6 @@ msgstr "Set percentage thresholds for meter colors."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1484,7 +1481,6 @@ msgstr "To email(s)"
msgid "Toggle grid"
msgstr "Toggle grid"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Toggle theme"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-14 09:39\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Alertas"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Todos los contenedores"
@@ -838,7 +837,6 @@ msgstr "Inactivo"
msgid "Invalid email address."
msgstr "Dirección de correo electrónico no válida."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Idioma"
@@ -1309,7 +1307,7 @@ msgstr "Buscar sistemas o configuraciones..."
#: 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."
msgstr "Consulta <0>configuración de notificaciones</0> para configurar cómo recibe alertas."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
@@ -1337,7 +1335,6 @@ msgstr "Establecer umbrales de porcentaje para los colores de los medidores."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "A correo(s)"
msgid "Toggle grid"
msgstr "Alternar cuadrícula"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Alternar tema"
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "هشدارها"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "همه کانتینرها"
@@ -838,7 +837,6 @@ msgstr "غیرفعال"
msgid "Invalid email address."
msgstr "آدرس ایمیل نامعتبر است."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "زبان"
@@ -1337,7 +1335,6 @@ msgstr "آستانه های درصدی را برای رنگ های متر تنظ
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "به ایمیل(ها)"
msgid "Toggle grid"
msgstr "تغییر نمایش جدول"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "تغییر تم"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2026-01-09 21:08\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -157,7 +157,6 @@ msgstr "Alertes"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Tous les conteneurs"
@@ -231,7 +230,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
@@ -838,7 +837,6 @@ msgstr "Inactif"
msgid "Invalid email address."
msgstr "Adresse email invalide."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Langue"
@@ -1337,7 +1335,6 @@ msgstr "Définir des seuils de pourcentage pour les couleurs des compteurs."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "Aux email(s)"
msgid "Toggle grid"
msgstr "Basculer la grille"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Changer le thème"
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: he\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -157,7 +157,6 @@ msgstr "התראות"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "כל הקונטיינרים"
@@ -838,7 +837,6 @@ msgstr "לא פעיל"
msgid "Invalid email address."
msgstr "כתובת אימייל לא תקינה."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "שפה"
@@ -1337,7 +1335,6 @@ msgstr "הגדר סף אחוזים עבור צבעי מד."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "לאימייל(ים)"
msgid "Toggle grid"
msgstr "החלף רשת"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "החלף ערכת נושא"
+54 -58
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -99,7 +99,7 @@ msgstr "Aktivan"
#: src/components/active-alerts.tsx
msgid "Active Alerts"
msgstr "Aktivna Upozorenja"
msgstr "Aktivna upozorenja"
#: src/components/systemd-table/systemd-table.tsx
msgid "Active state"
@@ -113,11 +113,11 @@ msgstr "Dodaj {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Dodaj <0>Sustav</0>"
msgstr "Dodaj <0>Sistem</0>"
#: src/components/add-system.tsx
msgid "Add system"
msgstr "Dodaj sustav"
msgstr "Dodaj sistem"
#: src/components/routes/settings/notifications.tsx
msgid "Add URL"
@@ -125,7 +125,7 @@ msgstr "Dodaj URL"
#: src/components/routes/settings/general.tsx
msgid "Adjust display options for charts."
msgstr "Podesite opcije prikaza grafikona."
msgstr "Podesite opcije prikaza za grafikone."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
@@ -157,7 +157,6 @@ msgstr "Upozorenja"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Svi spremnici"
@@ -169,7 +168,7 @@ msgstr "Svi spremnici"
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Svi Sustavi"
msgstr "Svi Sistemi"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Are you sure you want to delete {name}?"
@@ -207,7 +206,7 @@ msgstr "Prosječna potrošnja energije grafičkog procesora"
#: src/components/routes/system.tsx
msgid "Average system-wide CPU utilization"
msgstr "Prosječna iskorištenost procesora u cijelom sustavu"
msgstr "Prosječna iskorištenost procesora na cijelom sustavu"
#. placeholder {0}: gpu.n
#: src/components/routes/system.tsx
@@ -216,7 +215,7 @@ msgstr "Prosječna iskorištenost {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr "Prosječna iskorištenost grafičkih procesora"
msgstr "Prosječna iskorištenost GPU motora"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
@@ -226,12 +225,12 @@ msgstr "Sigurnosne kopije"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Bandwidth"
msgstr "Mrežna Propusnost"
msgstr "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
@@ -259,11 +258,11 @@ msgstr "Ispod {0}{1} u posljednjih {2, plural, one {# minuti} few {# minute} oth
#: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "Beszel podržava OpenID Connect i mnoge druge pružatelje OAuth2 autentifikacije."
msgstr "Beszel podržava OpenID Connect i mnoge druge OAuth2 davatalje autentifikacije."
#: src/components/routes/settings/notifications.tsx
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
msgstr "Beszel koristi <0>Shoutrrr</0> za integraciju s popularnim obavještajnim uslugama."
msgstr "Beszel koristi <0>Shoutrrr</0> za integraciju sa popularnim servisima za notifikacije."
#: src/components/add-system.tsx
msgid "Binary"
@@ -340,19 +339,19 @@ msgstr "Puni se"
#: src/components/routes/settings/general.tsx
msgid "Chart options"
msgstr "Postavke grafikona"
msgstr "Opcije grafikona"
#: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link."
msgstr "Provjerite {email} za pristup poveznici za resetiranje."
msgstr "Provjerite {email} za vezu za resetiranje."
#: src/components/routes/settings/layout.tsx
msgid "Check logs for more details."
msgstr "Provjerite zapise (logove) za više detalja."
msgstr "Provjerite logove za više detalja."
#: src/components/routes/settings/notifications.tsx
msgid "Check your notification service"
msgstr "Provjerite svoju obavještajnu uslugu"
msgstr "Provjerite Vaš servis notifikacija"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
@@ -388,7 +387,7 @@ msgstr "Konfigurirajte način primanja obavijesti upozorenja."
#: src/components/login/auth-form.tsx
#: src/components/login/auth-form.tsx
msgid "Confirm password"
msgstr "Potvrdi lozinku"
msgstr "Potvrdite lozinku"
#: src/components/systemd-table/systemd-table.tsx
msgid "Conflicts"
@@ -401,7 +400,7 @@ msgstr "Veza je pala"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
msgstr "Nastavi"
msgstr "Nastavite"
#: src/lib/utils.ts
msgid "Copied to clipboard"
@@ -578,15 +577,15 @@ msgstr "Iskorištenost diska od {extraFsName}"
#: src/components/routes/system.tsx
msgid "Docker CPU Usage"
msgstr "Iskorištenost Docker procesora"
msgstr "Iskorištenost Docker Procesora"
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Iskorištenost Docker memorije"
msgstr "Iskorištenost Docker Memorije"
#: src/components/routes/system.tsx
msgid "Docker Network I/O"
msgstr "Docker mrežni I/O"
msgstr "Docker Mrežni I/O"
#: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx
@@ -631,12 +630,12 @@ msgstr "Email"
#: src/components/routes/settings/notifications.tsx
msgid "Email notifications"
msgstr "Email obavijesti"
msgstr "Email notifikacije"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Empty"
msgstr "Prazno"
msgstr "Prazna"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -645,7 +644,7 @@ msgstr "Vrijeme završetka"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Unesite email adresu kako biste resetirali lozinku"
msgstr "Unesite email adresu za resetiranje lozinke"
#: src/components/routes/settings/notifications.tsx
msgid "Enter email address..."
@@ -653,7 +652,7 @@ msgstr "Unesite email adresu..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr "Unesite jednokratnu lozinku."
msgstr "Unesite Vašu jednokratnu lozinku."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Ephemeral"
@@ -683,7 +682,7 @@ msgstr "Glavni PID izvršavanja"
#: src/components/routes/settings/config-yaml.tsx
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
msgstr "Postojeći sustavi koji nisu definirani u <0>config.yml</0> datoteci bit će izbrisani. Molimo Vas da spremate redovite sigurnosne kopije."
msgstr "Postojeći sistemi koji nisu definirani u <0>config.yml</0> će biti izbrisani. Molimo Vas napravite redovite sigurnosne kopije."
#: src/components/systemd-table/systemd-table.tsx
msgid "Exited active"
@@ -695,7 +694,7 @@ msgstr "Istječe nakon jednog sata ili ponovnog pokretanja huba."
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
msgstr "Izvoz"
msgstr "Izvezi"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -719,21 +718,21 @@ msgstr "Neuspjeli atributi:"
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Neuspješna provjera autentičnosti"
msgstr "Provjera autentičnosti nije uspjela"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Neuspješno spremanje postavki"
msgstr "Neuspješno snimanje postavki"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Neuspješno slanje probne obavijesti"
msgstr "Neuspješno slanje testne notifikacije"
#: src/components/alerts/alerts-sheet.tsx
msgid "Failed to update alert"
msgstr "Neuspješno ažuriranje upozorenja"
msgstr "Ažuriranje upozorenja nije uspjelo"
#. placeholder {0}: statusTotals[ServiceStatus.Failed]
#: src/components/systemd-table/systemd-table.tsx
@@ -751,7 +750,7 @@ msgstr "Filtriraj..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr "Otisak"
msgstr "Otisak prsta"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
@@ -774,7 +773,7 @@ msgstr "FreeBSD naredba"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "Puno"
msgstr "Puna"
#. Context: General settings
#: src/components/routes/settings/general.tsx
@@ -788,7 +787,7 @@ msgstr "Globalno"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Grafički procesori"
msgstr "GPU motori"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
@@ -800,7 +799,7 @@ msgstr "Iskorištenost GPU-a"
#: src/components/systems-table/systems-table.tsx
msgid "Grid"
msgstr "Rešetka"
msgstr "Mreža"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
@@ -819,7 +818,7 @@ msgstr "Host / IP"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr "Neaktivno"
msgstr "Neaktivna"
#: 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."
@@ -836,9 +835,8 @@ msgstr "Neaktivno"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Nevažeća email adresa."
msgstr "Nevažeća adresa e-pošte."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Jezik"
@@ -879,7 +877,7 @@ msgstr "Prosječno Opterećenje 5m"
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg"
msgstr "Prosječno Opterećenje"
msgstr "Prosječno opterećenje"
#: src/components/systemd-table/systemd-table.tsx
msgid "Load state"
@@ -900,13 +898,13 @@ msgstr "Prijava"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
msgid "Login attempt failed"
msgstr "Neuspješno pokušaj prijave"
msgstr "Pokušaj prijave nije uspio"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
msgid "Logs"
msgstr "Zapisi"
msgstr "Logovi"
#: src/components/routes/settings/notifications.tsx
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
@@ -950,15 +948,15 @@ msgstr "Vrhunac memorije"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage"
msgstr "Iskorištenost memorije"
msgstr "Upotreba memorije"
#: src/components/routes/system.tsx
msgid "Memory usage of docker containers"
msgstr "Iskorištenost memorije Docker spremnika"
msgstr "Upotreba 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
@@ -1030,7 +1028,7 @@ msgstr "Podrška za OAuth 2 / OIDC"
#: src/components/routes/settings/config-yaml.tsx
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka bit će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka biti će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -1047,11 +1045,11 @@ msgstr "Jednokratna lozinka"
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Otvori meni"
msgstr "Otvori menu"
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Ili nastavi s"
msgstr "Ili nastavi sa"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Other"
@@ -1059,7 +1057,7 @@ msgstr "Ostalo"
#: src/components/alerts/alerts-sheet.tsx
msgid "Overwrite existing alerts"
msgstr "Prebriši postojeća upozorenja"
msgstr "Prebrišite postojeća upozorenja"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
@@ -1092,7 +1090,7 @@ msgstr "Lozinka mora biti kraća od 72 bajta."
#: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received"
msgstr "Zahtjev za ponovno postavljanje lozinke zaprimljen"
msgstr "Zahtjev za ponovno postavljanje lozinke primljen"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
@@ -1133,20 +1131,20 @@ msgstr "Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku u
#: src/components/alerts/alerts-sheet.tsx
msgid "Please check logs for more details."
msgstr "Za više detalja provjerite zapise (logove)."
msgstr "Za više detalja provjerite logove."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
msgid "Please check your credentials and try again"
msgstr "Provjerite svoje vjerodajnice i pokušajte ponovno"
msgstr "Provjerite svoje podatke i pokušajte ponovno"
#: src/components/login/login.tsx
msgid "Please create an admin account"
msgstr "Molimo kreirajte administrativan račun"
msgstr "Molimo kreirajte administratorski račun"
#: src/components/login/auth-form.tsx
msgid "Please enable pop-ups for this site"
msgstr "Molimo omogućite skočne prozore za ovu stranicu"
msgstr "Omogućite skočne prozore za ovu stranicu"
#: src/lib/api.ts
msgid "Please log in again"
@@ -1154,7 +1152,7 @@ msgstr "Molimo prijavite se ponovno"
#: src/components/login/auth-form.tsx
msgid "Please see <0>the documentation</0> for instructions."
msgstr "Molimo provjerite <0>dokumentaciju</0> za upute."
msgstr "Molimo pogledajte <0>dokumentaciju</0> za instrukcije."
#: src/components/login/login.tsx
msgid "Please sign in to your account"
@@ -1337,7 +1335,6 @@ msgstr "Postavite pragove postotka za boje mjerača."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1415,7 +1412,7 @@ msgstr "Systemd servisi"
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Sustavi"
msgstr "Sistemi"
#: src/components/routes/settings/config-yaml.tsx
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
@@ -1489,7 +1486,6 @@ msgstr "Primaoci e-pošte"
msgid "Toggle grid"
msgstr "Uključi/isključi rešetku"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Uključi/isključi temu"
+17 -21
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -113,7 +113,7 @@ msgstr "Hozzáadás {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "<0>Rendszer</0> Hozzáadása"
msgstr "Hozzáadás <0>System</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -125,7 +125,7 @@ msgstr "URL hozzáadása"
#: src/components/routes/settings/general.tsx
msgid "Adjust display options for charts."
msgstr "A diagramok megjelenítésének beállítása."
msgstr "Állítsa be a diagram megjelenítését."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
@@ -157,10 +157,9 @@ msgstr "Riasztások"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Minden konténer"
msgstr "Összes konténer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
@@ -231,7 +230,7 @@ msgstr "Sávszélesség"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr "Akku"
msgstr "Akk"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -657,7 +656,7 @@ msgstr "Adja meg az egyszeri jelszavát."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Ephemeral"
msgstr "Ideiglenes"
msgstr "Átmeneti"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -759,7 +758,7 @@ msgstr "Firmware"
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "<0>{min}</0> {min, plural, one {percig} other {percig}}"
msgstr "A <0>{min}</0> {min, plural, one {perc} other {percek}}"
#: src/components/login/auth-form.tsx
msgid "Forgot password?"
@@ -838,7 +837,6 @@ msgstr "Inaktív"
msgid "Invalid email address."
msgstr "Érvénytelen e-mail cím."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Nyelv"
@@ -1125,7 +1123,7 @@ msgstr "Állandó"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence"
msgstr "Tartósság"
msgstr "Kitartás"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -1313,7 +1311,7 @@ msgstr "Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogy
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "{foo} kiválasztása"
msgstr ""
#: src/components/routes/system.tsx
msgid "Sent"
@@ -1337,7 +1335,6 @@ msgstr "Százalékos küszöbértékek beállítása a mérőszínekhez."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "E-mailben"
msgid "Toggle grid"
msgstr "Rács ki- és bekapcsolása"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Téma váltása"
@@ -1553,19 +1549,19 @@ msgstr "Riaszt, ha az 5 perces terhelési átlag túllép egy küszöbértéket"
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Riaszt, ha bármelyik hőmérséklet érzékelő túllép egy küszöbértéket"
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
#: src/lib/alerts.ts
msgid "Triggers when battery charge drops below a threshold"
msgstr "Riaszt, ha az akkumulátor töltöttségi szintje egy küszöbérték alá esik"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Riaszt, ha a sávszélesség-használat túllép egy küszöbértéket"
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
#: src/lib/alerts.ts
msgid "Triggers when CPU usage exceeds a threshold"
msgstr "Riaszt, ha a CPU használat túllép egy küszöbértéket"
msgstr "Bekapcsol, ha a CPU érzékelő túllép egy küszöbértéket"
#: src/lib/alerts.ts
msgid "Triggers when GPU usage exceeds a threshold"
@@ -1573,15 +1569,15 @@ msgstr "Riaszt, ha a GPU használat túllép egy küszöbértéket"
#: src/lib/alerts.ts
msgid "Triggers when memory usage exceeds a threshold"
msgstr "Riaszt, ha a memóriahasználat túllép egy küszöbértéket"
msgstr "Bekapcsol, ha a Ram érzékelő túllép egy küszöbértéket"
#: src/lib/alerts.ts
msgid "Triggers when status switches between up and down"
msgstr "Riaszt, amikor a rendszer online állapota változik"
msgstr "Bekapcsol, amikor az állapot fel és le között változik"
#: src/lib/alerts.ts
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Riaszt, ha a lemezhasználat túllép egy küszöbértéket"
msgstr "Bekapcsol, ha a lemez érzékelő túllép egy küszöbértéket"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -1714,7 +1710,7 @@ msgstr "Webhook / Push értesítések"
#: src/components/routes/settings/tokens-fingerprints.tsx
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."
msgstr "Ha engedélyezve van, ez a token lehetővé teszi az ügynökök számára az önregisztrációt rendszer előzetes létrehozása nélkül."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
+27 -31
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: id\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-25 10:45\n"
"Last-Translator: \n"
"Language-Team: Indonesian\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -157,7 +157,6 @@ msgstr "Peringatan"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Semua Container"
@@ -457,7 +456,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 +542,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 +555,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 +585,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 +606,7 @@ msgstr "Mati ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr ""
msgstr "Download"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -627,7 +626,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"
@@ -668,7 +667,7 @@ msgstr "Sementara"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
msgstr ""
msgstr "Error"
#. placeholder {0}: alert.value
#. placeholder {1}: info.unit
@@ -695,7 +694,7 @@ msgstr "Kedaluwarsa setelah satu jam atau saat restart hub."
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
msgstr "Ekspor"
msgstr "Export"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -707,7 +706,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"
@@ -747,15 +746,15 @@ msgstr "Gagal: {0}"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Filter..."
msgstr ""
msgstr "Filter..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr "Sidik jari"
msgstr "Fingerprint"
#: 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 +783,7 @@ msgstr "Umum"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr ""
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
@@ -814,12 +813,12 @@ msgstr "Perintah Homebrew"
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr ""
msgstr "Host / IP"
#. 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,7 +827,7 @@ msgstr "Jika anda kehilangan kata sandi untuk akun admin anda, anda dapat merese
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Docker image"
msgid "Image"
msgstr ""
msgstr "Image"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
@@ -838,7 +837,6 @@ msgstr "Tidak aktif"
msgid "Invalid email address."
msgstr "Alamat email tidak valid."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Bahasa"
@@ -891,7 +889,7 @@ msgstr "Memuat..."
#: src/components/navbar.tsx
msgid "Log Out"
msgstr "Keluar"
msgstr "Log Out"
#: src/components/login/login.tsx
msgid "Login"
@@ -958,7 +956,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
@@ -1162,7 +1160,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 +1246,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"
@@ -1337,7 +1335,6 @@ msgstr "Tetapkan ambang persentase untuk warna meter."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1377,7 +1374,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"
@@ -1489,7 +1486,6 @@ msgstr "Ke email"
msgid "Toggle grid"
msgstr "Ganti tampilan"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Ganti tema"
@@ -1497,7 +1493,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 +1512,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 +1525,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,7 +1635,7 @@ msgstr "Diperbarui setiap 10 menit."
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr ""
msgstr "Upload"
#: src/components/routes/system/info-bar.tsx
msgid "Uptime"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:17\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Avvisi"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Tutti i contenitori"
@@ -231,7 +230,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
@@ -838,7 +837,6 @@ msgstr "Inattivo"
msgid "Invalid email address."
msgstr "Indirizzo email non valido."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Lingua"
@@ -1337,7 +1335,6 @@ msgstr "Imposta le soglie percentuali per i colori dei contatori."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "A email(s)"
msgid "Toggle grid"
msgstr "Attiva/disattiva griglia"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Attiva/disattiva tema"
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -157,7 +157,6 @@ msgstr "アラート"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "すべてのコンテナ"
@@ -838,7 +837,6 @@ msgstr "非アクティブ"
msgid "Invalid email address."
msgstr "無効なメールアドレスです。"
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "言語"
@@ -1337,7 +1335,6 @@ msgstr "メーターの色を変更するしきい値(パーセンテージ)
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "宛先メールアドレス"
msgid "Toggle grid"
msgstr "グリッドを切り替え"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "テーマを切り替え"
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -157,7 +157,6 @@ msgstr "알림"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "모든 컨테이너"
@@ -838,7 +837,6 @@ msgstr "비활성"
msgid "Invalid email address."
msgstr "잘못된 이메일 주소입니다."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "언어"
@@ -1337,7 +1335,6 @@ msgstr "그래프 미터 색상의 백분율 임계값을 설정합니다."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "받는사람(들)"
msgid "Toggle grid"
msgstr "그리드 전환"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "테마 전환"
+14 -18
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-17 12:02\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -59,7 +59,7 @@ msgstr "1 minuut"
#: src/lib/utils.ts
msgid "1 week"
msgstr "1 week"
msgstr ""
#: src/lib/utils.ts
msgid "12 hours"
@@ -134,7 +134,7 @@ msgstr "Breedte van het hoofdlayout aanpassen"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
msgstr "Administrator"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "After"
@@ -142,7 +142,7 @@ msgstr "Na"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -157,7 +157,6 @@ msgstr "Waarschuwingen"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Alle containers"
@@ -272,7 +271,7 @@ msgstr "Binair"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bits (Kbps, Mbps, Gbps)"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Boot state"
@@ -281,11 +280,11 @@ msgstr "Opstartstatus"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)"
msgstr ""
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
msgstr "Cache / Buffers"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Can reload"
@@ -319,7 +318,7 @@ msgstr "Opgelet - potentieel gegevensverlies"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr "Celsius (°C)"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
@@ -457,7 +456,7 @@ msgstr "YAML kopiëren"
#: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr "CPU"
msgstr ""
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
@@ -707,7 +706,7 @@ msgstr "Exporteer je huidige systeemconfiguratie."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
@@ -838,7 +837,6 @@ msgstr "Inactief"
msgid "Invalid email address."
msgstr "Ongeldig e-mailadres."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Taal"
@@ -928,7 +926,7 @@ msgstr "Handmatige installatie-instructies"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
msgid "Max 1 min"
msgstr "Max 1 min"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/info-bar.tsx
@@ -1121,7 +1119,7 @@ msgstr "Percentage tijd besteed in elke status"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent"
msgstr "Blijvend"
msgstr "Permanent"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence"
@@ -1337,7 +1335,6 @@ msgstr "Stel percentagedrempels in voor meterkleuren."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1377,7 +1374,7 @@ msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
msgstr ""
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Sub State"
@@ -1450,7 +1447,7 @@ msgstr "Temperatuur van systeem sensoren"
#: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
@@ -1489,7 +1486,6 @@ msgstr "Naar e-mail(s)"
msgid "Toggle grid"
msgstr "Schakel raster"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Schakel thema"
+4 -8
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-05 20:24\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Alarmer"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Alle containere"
@@ -231,7 +230,7 @@ msgstr "Båndbredde"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr "Batteri"
msgstr ""
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -838,7 +837,6 @@ msgstr "Inaktiv"
msgid "Invalid email address."
msgstr "Ugyldig e-postadresse."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Språk"
@@ -910,7 +908,7 @@ msgstr "Logger"
#: src/components/routes/settings/notifications.tsx
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
msgstr "Ser du etter hvor du kan opprette alarmer? Klikk på bjelle-ikonet <0/> for systemet i systemoversikten."
msgstr "Ser du etter hvor du kan opprette alarmer? Klikk på bjelle-ikonene <0/> i systemtabellen."
#: src/components/systemd-table/systemd-table.tsx
msgid "Main PID"
@@ -1337,7 +1335,6 @@ msgstr "Angi prosentvise terskler for målerfarger."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "Til e-postadresse(r)"
msgid "Toggle grid"
msgstr "Rutenett av/på"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Tema av/på"
@@ -1553,7 +1549,7 @@ msgstr "Slår inn når gjennomsnittsbelastningen over 5 minutter overstiger en g
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Slår inn når hvilken som helst sensor overstiger en grenseverdi"
msgstr "Slår inn når enhver sensor overstiger en grenseverdi"
#: src/lib/alerts.ts
msgid "Triggers when battery charge drops below a threshold"
+39 -43
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-18 19:21\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -129,7 +129,7 @@ msgstr "Dostosuj opcje wyświetlania wykresów."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Dostosuj szerokość widoku"
msgstr "Dostosuj szerokość głównego układu"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
@@ -157,7 +157,6 @@ msgstr "Alerty"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Wszystkie kontenery"
@@ -189,12 +188,12 @@ msgstr "Średnia"
#: src/components/routes/system.tsx
msgid "Average CPU utilization of containers"
msgstr "Średnie wykorzystanie CPU przez kontenery"
msgstr "Średnie wykorzystanie procesora przez kontenery"
#. placeholder {0}: alertData.unit
#: src/components/alerts/alerts-sheet.tsx
msgid "Average drops below <0>{value}{0}</0>"
msgstr "Średnia spada poniżej <0>{value}{0}</0>"
msgstr ""
#. placeholder {0}: alertData.unit
#: src/components/alerts/alerts-sheet.tsx
@@ -207,7 +206,7 @@ msgstr "Średnie zużycie energii przez GPU"
#: src/components/routes/system.tsx
msgid "Average system-wide CPU utilization"
msgstr "Średnie wykorzystanie CPU w całym systemie"
msgstr "Średnie wykorzystanie procesora w całym systemie"
#. placeholder {0}: gpu.n
#: src/components/routes/system.tsx
@@ -221,7 +220,7 @@ msgstr "Średnie wykorzystanie silników GPU"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
msgstr "Kopie zapasowe"
msgstr "Kopie"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -231,7 +230,7 @@ msgstr "Przepustowość"
#. Battery label in systems table header
#: src/components/systems-table/systems-table-columns.tsx
msgid "Bat"
msgstr "Bateria"
msgstr ""
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -255,7 +254,7 @@ msgstr "Przed"
#. placeholder {2}: alert.min
#: src/components/active-alerts.tsx
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
msgstr "Poniżej {0}{1} w ciągu ostatnich {2, plural, one {# minuty} other {# minut}}"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
@@ -263,7 +262,7 @@ msgstr "Beszel obsługuje OpenID Connect i wielu dostawców uwierzytelniania OAu
#: src/components/routes/settings/notifications.tsx
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
msgstr "Beszel używa <0>Shoutrrr</0> do integracji z popularnymi serwisami powiadomień."
msgstr "Beszel używa <0>Shoutrrr</0> do integracji z popularnych serwisów powiadomień."
#: src/components/add-system.tsx
msgid "Binary"
@@ -315,7 +314,7 @@ msgstr "Pojemność"
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr "Uwaga - ryzyko utraty danych"
msgstr "Uwaga- potencjalna utrata danych."
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
@@ -327,7 +326,7 @@ msgstr "Zmień jednostki wyświetlania dla metryk."
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
msgstr "Zmi ogólne ustawienia aplikacji."
msgstr "Zmiana ogólnych ustawień aplikacji."
#: src/components/routes/system.tsx
msgid "Charge"
@@ -340,7 +339,7 @@ msgstr "Ładuje się"
#: src/components/routes/settings/general.tsx
msgid "Chart options"
msgstr "Wykresy"
msgstr "Opcje wykresu"
#: src/components/login/forgot-pass-form.tsx
msgid "Check {email} for a reset link."
@@ -362,15 +361,15 @@ msgstr "Wyczyść"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Wybierz kontener, aby wyświetlić więcej informacji."
msgstr "Kliknij na kontener, aby wyświetlić więcej informacji."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
msgstr "Wybierz urządzenie, aby wyświetlić więcej informacji."
msgstr "Kliknij na urządzenie, aby wyświetlić więcej informacji."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Wybierz system, aby wyświetlić więcej informacji."
msgstr "Kliknij na system, aby zobaczyć więcej informacji."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
@@ -501,11 +500,11 @@ msgstr "Krytyczny (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr "Pobieranie łącznie"
msgstr "Pobieranie skumulowane"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr "Wysyłanie łącznie"
msgstr "Wysyłanie skumulowane"
#. Context: Battery state
#: src/components/routes/system.tsx
@@ -578,11 +577,11 @@ msgstr "Wykorzystanie dysku {extraFsName}"
#: src/components/routes/system.tsx
msgid "Docker CPU Usage"
msgstr "Użycie CPU przez Docker"
msgstr "Wykorzystanie procesora przez Docker"
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Użycie pamięci przez Docker"
msgstr "Wykorzystanie pamięci przez Docker"
#: src/components/routes/system.tsx
msgid "Docker Network I/O"
@@ -657,7 +656,7 @@ msgstr "Wprowadź swoje jednorazowe hasło."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Ephemeral"
msgstr "Tymczasowy"
msgstr "Efemeryczny"
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -683,7 +682,7 @@ msgstr "Główny PID wykonania"
#: src/components/routes/settings/config-yaml.tsx
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
msgstr "Istniejące systemy, które nie są zdefiniowane w <0>config.yml</0>, zostaną usunięte. Pamiętaj aby regularnie tworzyć kopie zapasowe."
msgstr "Istniejące systemy, które nie są zdefiniowane w <0>config.yml</0>, zostaną usunięte. Proszę regularnie tworzyć kopie zapasowe."
#: src/components/systemd-table/systemd-table.tsx
msgid "Exited active"
@@ -729,7 +728,7 @@ msgstr "Nie udało się zapisać ustawień"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
msgstr "Nie udało się wysłać powiadomienia testowego"
msgstr "Nie udało się wysłać testowego powiadomienia"
#: src/components/alerts/alerts-sheet.tsx
msgid "Failed to update alert"
@@ -804,7 +803,7 @@ msgstr "Siatka"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
msgstr "Kondycja"
msgstr "Zdrowie"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -838,7 +837,6 @@ msgstr "Nieaktywny"
msgid "Invalid email address."
msgstr "Nieprawidłowy adres e-mail."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Język"
@@ -866,15 +864,15 @@ msgstr "Średnie obciążenie"
#: src/lib/alerts.ts
msgid "Load Average 15m"
msgstr "Średnie obciążenie 15 min"
msgstr "Średnie obciążenie 15 m"
#: src/lib/alerts.ts
msgid "Load Average 1m"
msgstr "Średnie obciążenie 1 min"
msgstr "Średnie obciążenie 1 m"
#: src/lib/alerts.ts
msgid "Load Average 5m"
msgstr "Średnie obciążenie 5 min"
msgstr "Średnie obciążenie 5 m"
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
@@ -950,7 +948,7 @@ msgstr "Szczyt pamięci"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage"
msgstr "Użycie pamięci"
msgstr "Wykorzystanie pamięci"
#: src/components/routes/system.tsx
msgid "Memory usage of docker containers"
@@ -998,7 +996,7 @@ msgstr "Nie"
#: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "No results found."
msgstr "Nie znaleziono wyników."
msgstr "Brak wyników."
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
@@ -1084,7 +1082,7 @@ msgstr "Hasło"
#: src/components/login/auth-form.tsx
msgid "Password must be at least 8 characters."
msgstr "Hasło musi zawierać co najmniej 8 znaków."
msgstr "Hasło musi mieć co najmniej 8 znaków."
#: src/components/login/auth-form.tsx
msgid "Password must be less than 72 bytes."
@@ -1092,11 +1090,11 @@ msgstr "Hasło musi być mniejsze niż 72 bajty."
#: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received"
msgstr "Otrzymano żądanie resetowania hasła"
msgstr "Otrzymane żądanie resetowania hasła"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Poprzednie"
msgstr "Przeszłe"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
@@ -1239,7 +1237,7 @@ msgstr "Rozwiązany"
#: src/components/systemd-table/systemd-table.tsx
msgid "Restarts"
msgstr "Uruchamia ponownie"
msgstr "Ponowne uruchomienia"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Resume"
@@ -1285,7 +1283,7 @@ msgstr "Zapisz system"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it."
msgstr "Zapisany w bazie danych. Nie wygasa, dopóki go nie wyłączysz."
msgstr "Zapisane w bazie danych i nie wygasa, dopóki go nie wyłączysz."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
@@ -1337,7 +1335,6 @@ msgstr "Ustaw progi procentowe dla kolorów mierników."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1345,7 +1342,7 @@ msgstr "Ustawienia"
#: src/components/routes/settings/layout.tsx
msgid "Settings saved"
msgstr "Ustawienia zostały zapisane"
msgstr "Ustawienia zapisane"
#: src/components/login/auth-form.tsx
msgid "Sign in"
@@ -1487,9 +1484,8 @@ msgstr "Do e-mail(ów)"
#: src/components/routes/system/info-bar.tsx
#: src/components/routes/system/info-bar.tsx
msgid "Toggle grid"
msgstr "Przełącz widok"
msgstr "Przełącz siatkę"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Zmień motyw"
@@ -1557,7 +1553,7 @@ msgstr "Wyzwalane, gdy jakikolwiek czujnik przekroczy ustalony próg."
#: src/lib/alerts.ts
msgid "Triggers when battery charge drops below a threshold"
msgstr "Uruchamia się, gdy poziom baterii spadnie poniżej wybranej wartości"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when combined up/down exceeds a threshold"
@@ -1573,7 +1569,7 @@ msgstr "Wyzwalane, gdy użycie GPU przekroczy próg"
#: src/lib/alerts.ts
msgid "Triggers when memory usage exceeds a threshold"
msgstr "Wyzwalane, wykorzystanie pamięci przekroczy ustalony próg"
msgstr "Wyzwalane, wykorzystanie pamięci przekroczy ustalony próg."
#: src/lib/alerts.ts
msgid "Triggers when status switches between up and down"
@@ -1694,7 +1690,7 @@ msgstr "Oczekiwanie na wystarczającą liczbę rekordów do wyświetlenia"
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "Chcesz pomóc ulepszyć nasze tłumaczenie? Sprawdź <0>Crowdin</0> po szczegóły."
msgstr "Chcesz pomóc nam uczynić nasze tłumaczenia jeszcze lepszymi? Sprawdź <0>Crowdin</0> po więcej szczegółów."
#: src/components/systemd-table/systemd-table.tsx
msgid "Wants"
@@ -1726,7 +1722,7 @@ msgstr "Polecenie Windows"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Write"
msgstr "Zapis"
msgstr "Napisz"
#: src/components/routes/settings/layout.tsx
msgid "YAML Config"
+7 -11
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-05 20:24\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -42,7 +42,7 @@ msgstr "{count, plural, one {{countString} minuto} other {{countString} minutos}
#: src/components/routes/system/info-bar.tsx
msgid "{threads, plural, one {# thread} other {# threads}}"
msgstr ""
msgstr "{threads, plural, one {# thread} other {# threads}}"
#: src/lib/utils.ts
msgid "1 hour"
@@ -157,7 +157,6 @@ msgstr "Alertas"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Todos os Contêineres"
@@ -231,7 +230,7 @@ msgstr "Largura de 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
@@ -285,7 +284,7 @@ msgstr "Bytes (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
msgstr "Cache / Buffers"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Can reload"
@@ -814,7 +813,7 @@ msgstr "Comando Homebrew"
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr "Host / IP"
msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
@@ -838,7 +837,6 @@ msgstr "Inativo"
msgid "Invalid email address."
msgstr "Endereço de email inválido."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Idioma"
@@ -1337,7 +1335,6 @@ msgstr "Defina os limiares de porcentagem para as cores do medidor."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1433,7 +1430,7 @@ msgstr "Tarefas"
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Temp"
msgstr "Temp"
msgstr ""
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -1489,7 +1486,6 @@ msgstr "Para email(s)"
msgid "Toggle grid"
msgstr "Alternar grade"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Alternar tema"
@@ -1497,7 +1493,7 @@ msgstr "Alternar tema"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:17\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -157,7 +157,6 @@ msgstr "Оповещения"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Все контейнеры"
@@ -231,7 +230,7 @@ msgstr "Пропускная способность"
#. 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
@@ -838,7 +837,6 @@ msgstr "Неактивно"
msgid "Invalid email address."
msgstr "Неверный адрес электронной почты."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Язык"
@@ -1337,7 +1335,6 @@ msgstr "Установите процентные пороги для цвето
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "На электронную почту"
msgid "Toggle grid"
msgstr "Переключить сетку"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Переключить тему"
+5 -9
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-05 20:24\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -142,7 +142,7 @@ msgstr "Po"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr ""
msgstr "Agent"
#: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -157,7 +157,6 @@ msgstr "Opozorila"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Vsi kontejnerji"
@@ -231,7 +230,7 @@ msgstr "Pasovna širina"
#. 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
@@ -457,7 +456,7 @@ msgstr "Kopiraj 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"
@@ -707,7 +706,7 @@ msgstr "Izvozi trenutne nastavitve sistema."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr ""
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
@@ -838,7 +837,6 @@ msgstr "Neaktivno"
msgid "Invalid email address."
msgstr "Napačen e-poštni naslov."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Jezik"
@@ -1337,7 +1335,6 @@ msgstr "Nastavite odstotne pragove za barve merilnikov."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "E-pošta za"
msgid "Toggle grid"
msgstr "Preklopi način mreže"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Obrni temo"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-08 18:22\n"
"Last-Translator: \n"
"Language-Team: Serbian (Cyrillic)\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -157,7 +157,6 @@ msgstr "Упозорења"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Сви контејнери"
@@ -231,7 +230,7 @@ msgstr "Пропусни опсег"
#. 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
@@ -838,7 +837,6 @@ msgstr "Неактивно"
msgid "Invalid email address."
msgstr "Неважећа имејл адреса."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Језик"
@@ -1337,7 +1335,6 @@ msgstr "Подесите процентуалне прагове за боје
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "На е-пошту(е)"
msgid "Toggle grid"
msgstr "Укључи/искључи мрежу"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Промени тему"
+3 -7
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sv\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-05 20:24\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Larm"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Alla behållare"
@@ -231,7 +230,7 @@ msgstr "Bandbredd"
#. 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
@@ -838,7 +837,6 @@ msgstr "Inaktiv"
msgid "Invalid email address."
msgstr "Ogiltig e-postadress."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Språk"
@@ -1121,7 +1119,7 @@ msgstr "Procentandel av tid spenderad i varje tillstånd"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent"
msgstr ""
msgstr "Permanent"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence"
@@ -1337,7 +1335,6 @@ msgstr "Ställ in procentuella tröskelvärden för mätarfärger."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "Till e-postadress(er)"
msgid "Toggle grid"
msgstr "Växla rutnät"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Växla tema"
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: tr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -157,7 +157,6 @@ msgstr "Uyarılar"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Tüm Konteynerler"
@@ -838,7 +837,6 @@ msgstr "Pasif"
msgid "Invalid email address."
msgstr "Geçersiz e-posta adresi."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Dil"
@@ -1337,7 +1335,6 @@ msgstr "Sayaç renkleri için yüzde eşiklerini ayarlayın."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "E-posta(lar)a"
msgid "Toggle grid"
msgstr "Izgarayı değiştir"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Temayı değiştir"
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: uk\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -157,7 +157,6 @@ msgstr "Сповіщення"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Всі контейнери"
@@ -231,7 +230,7 @@ msgstr "Пропускна здатність"
#. 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
@@ -838,7 +837,6 @@ msgstr "Неактивне"
msgid "Invalid email address."
msgstr "Неправильна адреса електронної пошти."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Мова"
@@ -1337,7 +1335,6 @@ msgstr "Встановіть відсоткові пороги для кольо
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "На електронну пошту"
msgid "Toggle grid"
msgstr "Перемкнути сітку"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Перемкнути тему"
+4 -8
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: vi\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Vietnamese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -157,7 +157,6 @@ msgstr "Cảnh báo"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Tất cả container"
@@ -457,7 +456,7 @@ msgstr "Sao chép 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"
@@ -627,7 +626,7 @@ msgstr "Chỉnh sửa {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"
@@ -838,7 +837,6 @@ msgstr "Không hoạt động"
msgid "Invalid email address."
msgstr "Địa chỉ email không hợp lệ."
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "Ngôn ngữ"
@@ -1337,7 +1335,6 @@ msgstr "Đặt ngưỡng cho màu sắc đồng hồ."
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "Đến email(s)"
msgid "Toggle grid"
msgstr "Chuyển đổi lưới"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "Chuyển đổi chủ đề"
@@ -1497,7 +1493,7 @@ msgstr "Chuyển đổi chủ đề"
#: 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
+2 -6
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -113,7 +113,7 @@ msgstr "添加 {foo}"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "添加<0>客户端</0>"
msgstr "<0>添加客户端</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -157,7 +157,6 @@ msgstr "警报"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "所有容器"
@@ -838,7 +837,6 @@ msgstr "非活跃"
msgid "Invalid email address."
msgstr "无效的电子邮件地址。"
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "语言"
@@ -1337,7 +1335,6 @@ msgstr "设置仪表颜色的百分比阈值。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "发送到电子邮件"
msgid "Toggle grid"
msgstr "切换网格"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "切换主题"
+1 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2025-12-02 23:18\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -157,7 +157,6 @@ msgstr "警報"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "所有容器"
@@ -838,7 +837,6 @@ msgstr "未啟用"
msgid "Invalid email address."
msgstr "無效的電子郵件地址。"
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "語言"
@@ -1337,7 +1335,6 @@ msgstr "設定儀表顏色的百分比閾值。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "發送到電子郵件"
msgid "Toggle grid"
msgstr "切換網格"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "切換主題"
+15 -19
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-01-31 21:16\n"
"PO-Revision-Date: 2026-01-10 17:34\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -142,7 +142,7 @@ msgstr "之後"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
msgstr "代理"
#: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -157,7 +157,6 @@ msgstr "警報"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "所有容器"
@@ -443,11 +442,11 @@ msgstr "複製文字"
#: src/components/add-system.tsx
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
msgstr "以下方指令安裝 Agent ,或使用<0>通用 Token</0>進行自動註冊。"
msgstr "複製下面的代理程式安裝指令,或使用<0>通用令牌</0>自動註冊代理程式。"
#: src/components/add-system.tsx
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
msgstr "以下方<0>docker-compose.yml</0>執行 Agent,或使用<1>通用 Token</1>進行自動註冊。"
msgstr "複製下面的代理程式<0>docker-compose.yml</0>內容,或使用<1>通用令牌</1>自動註冊代理程式。"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML"
@@ -535,7 +534,7 @@ msgstr "刪除"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint"
msgstr "刪除 Fingerprint"
msgstr "刪除指紋"
#: src/components/systemd-table/systemd-table.tsx
msgid "Description"
@@ -691,7 +690,7 @@ msgstr "結束"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Expires after one hour or on hub restart."
msgstr "在一個小時後或重新啟動 Hub 時過期。"
msgstr "小時後或重新啟動集線器時過期。"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
@@ -751,7 +750,7 @@ msgstr "篩選..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr "Fingerprint"
msgstr "指紋"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
@@ -838,7 +837,6 @@ msgstr "未啟用"
msgid "Invalid email address."
msgstr "無效的電子郵件地址。"
#: src/components/lang-toggle.tsx
#: src/components/routes/settings/general.tsx
msgid "Language"
msgstr "語言"
@@ -1252,7 +1250,7 @@ msgstr "Root"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr "重設 Token"
msgstr "輪替令牌"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
@@ -1285,7 +1283,7 @@ msgstr "儲存系統"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Saved in the database and does not expire until you disable it."
msgstr "存在資料庫中,在您停用之前不會過期。"
msgstr "存在資料庫中,在您停用之前不會過期。"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
@@ -1337,7 +1335,6 @@ msgstr "設定儀表顏色的百分比閾值。"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/layout.tsx
msgid "Settings"
@@ -1489,7 +1486,6 @@ msgstr "發送到電子郵件"
msgid "Toggle grid"
msgstr "切換網格"
#: src/components/mode-toggle.tsx
#: src/components/mode-toggle.tsx
msgid "Toggle theme"
msgstr "切換主題"
@@ -1497,21 +1493,21 @@ msgstr "切換主題"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr "令牌"
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens & Fingerprints"
msgstr "Token & Fingerprint"
msgstr "令牌和指紋"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
msgstr "Token 用於 Agent 的連線和註冊。Fingerprint 則是每個系統唯一穩定識別碼,於初次連線時設定。"
msgstr "令牌允許代理程式連線和註冊。指紋是每個系統唯一穩定識別符,在首次連線時產生。"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Token 與 Fingerprint 用於驗證連往 Hub 的 WebSocket 連線。"
msgstr "令牌和指紋被用於驗證 Hub 的 WebSocket 連線。"
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
@@ -1601,7 +1597,7 @@ msgstr "單位偏好"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "通用 Token"
msgstr "通用令牌"
#. Context: Battery state
#: src/lib/i18n.ts
@@ -1714,7 +1710,7 @@ msgstr "Webhook / 推送通知"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation."
msgstr "啟用後,此 Token 可讓 Agent 自行註冊,無需預先在系統中建立項目。"
msgstr "啟用後,此令牌允許代理無需事先建立系統即可自行註冊。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
+4
View File
@@ -12,6 +12,7 @@ import Settings from "@/components/routes/settings/layout.tsx"
import { ThemeProvider } from "@/components/theme-provider.tsx"
import { Toaster } from "@/components/ui/toaster.tsx"
import { alertManager } from "@/lib/alerts"
import { containerAlertManager } from "@/lib/container-alerts"
import { pb, updateUserSettings } from "@/lib/api.ts"
import { dynamicActivate, getLocale } from "@/lib/i18n"
import { $authenticated, $copyContent, $direction, $publicKey, $userSettings } from "@/lib/stores.ts"
@@ -49,8 +50,11 @@ const App = memo(() => {
.then(alertManager.refresh)
// subscribe to new alert updates
.then(alertManager.subscribe)
// subscribe to container alerts
.then(() => containerAlertManager.subscribe())
return () => {
alertManager.unsubscribe()
containerAlertManager.unsubscribe()
systemsManager.unsubscribe()
}
}, [])
+27
View File
@@ -340,6 +340,33 @@ export interface AlertInfo {
export type AlertMap = Record<string, Map<string, AlertRecord>>
export interface ContainerAlertRecord extends RecordModel {
id: string
system: string
container: string
name: string
triggered: boolean
value: number
min: number
}
export interface ContainerAlertInfo {
name: () => string
unit: string
icon: any
desc: () => string
max?: number
min?: number
step?: number
start?: number
/** Single value description (when there's only one value, like status) */
singleDesc?: () => string
invert?: boolean
}
export type ContainerAlertMap = Record<string, Map<string, Map<string, ContainerAlertRecord>>>
export interface SmartData {
/** model family */
// mf?: string
-46
View File
@@ -1,49 +1,3 @@
## 0.18.3
- Add experimental sysfs AMD GPU collector. (#737, #1569)
- Update LibreHardwareMonitorLib to 0.9.5. (#1697)
- Improve container network stats accuracy.
- Fix `SHARE_ALL_SYSTEMS` for system_details, smart_devices, and systemd_services. (#1660)
- Parse ATA device statistics for temperature and future metrics. (#1689)
- Add `SMART_DEVICES_SEPARATOR` environment variable and allow drives with the same name to be added with different types (e.g. RAID controllers). (#1655)
- Add tooltips for navbar buttons. (#1636)
- Add icon button for mobile use. (#1687)
- Add tooltip to system name in systems table. (#1640)
- Improve CJK truncation in UI.
- Fix container uptime sorting edge case. (#1696)
- Remove stale systemd services from tracking after deletion. (#1594)
- Apply SELinux context after binary replacement. (#1678)
- Update honeypot field name and autofill ignores. (#1011)
- Write health_file to `/dev/shm` instead of `/tmp` if available. (#1455)
- Don't force lowercase text for active alerts. (#1682)
- Ensure battery current charge doesn't exceed full capacity. (#1668)
- Increase `smartctl --scan` timeout to 10 seconds. (#1465)
- Use name-only matching for unique S.M.A.R.T. devices. (#1655)
- Fix smartctlArgs call to use hasExistingData flag. (#1645)
- Ignore alt key combinations when navigating systems with arrow keys. (#1698)
- Update Go dependencies
## 0.18.2
- Add separate dynamically linked glibc build for Linux. (#1618)
+8 -27
View File
@@ -7,15 +7,13 @@ param (
[int]$Port = 45876,
[string]$AgentPath = "",
[string]$NSSMPath = "",
[switch]$ConfigureFirewall,
[ValidateSet("Auto", "Scoop", "WinGet")]
[string]$InstallMethod = "Auto"
[switch]$ConfigureFirewall
)
# Check if required parameters are provided
if ([string]::IsNullOrWhiteSpace($Key)) {
Write-Host "ERROR: SSH Key is required." -ForegroundColor Red
Write-Host "Usage: .\install-agent.ps1 -Key 'your-ssh-key-here' [-Token 'your-token-here'] [-Url 'your-hub-url-here'] [-Port port-number] [-InstallMethod Auto|Scoop|WinGet] [-ConfigureFirewall]" -ForegroundColor Yellow
Write-Host "Usage: .\install-agent.ps1 -Key 'your-ssh-key-here' [-Token 'your-token-here'] [-Url 'your-hub-url-here'] [-Port port-number] [-ConfigureFirewall]" -ForegroundColor Yellow
Write-Host "Note: Token and Url are optional for backwards compatibility with older hub versions." -ForegroundColor Yellow
exit 1
}
@@ -488,34 +486,18 @@ try {
Write-Host "2. Install WinGet and run this script again" -ForegroundColor Yellow
exit 1
}
if ($InstallMethod -eq "Scoop") {
if (-not (Test-CommandExists "scoop")) {
throw "InstallMethod is set to Scoop, but Scoop is not available in PATH."
}
if (Test-CommandExists "scoop") {
Write-Host "Using Scoop for installation..."
$AgentPath = Install-WithScoop -Key $Key -Port $Port
}
elseif ($InstallMethod -eq "WinGet") {
if (-not (Test-CommandExists "winget")) {
throw "InstallMethod is set to WinGet, but WinGet is not available in PATH."
}
elseif (Test-CommandExists "winget") {
Write-Host "Using WinGet for installation..."
$AgentPath = Install-WithWinGet -Key $Key -Port $Port
}
else {
if (Test-CommandExists "scoop") {
Write-Host "Using Scoop for installation..."
$AgentPath = Install-WithScoop -Key $Key -Port $Port
}
elseif (Test-CommandExists "winget") {
Write-Host "Using WinGet for installation..."
$AgentPath = Install-WithWinGet -Key $Key -Port $Port
}
else {
Write-Host "Neither Scoop nor WinGet is installed. Installing Scoop..."
$AgentPath = Install-WithScoop -Key $Key -Port $Port
}
Write-Host "Neither Scoop nor WinGet is installed. Installing Scoop..."
$AgentPath = Install-WithScoop -Key $Key -Port $Port
}
}
@@ -579,8 +561,7 @@ try {
"-Token", "`"$Token`"",
"-Url", "`"$Url`"",
"-Port", $Port,
"-AgentPath", "`"$AgentPath`"",
"-InstallMethod", $InstallMethod
"-AgentPath", "`"$AgentPath`""
)
# Add NSSMPath if we found it