Compare commits

..

9 Commits

Author SHA1 Message Date
henrygd
5db4eb4346 0.18.6 release 2026-03-29 13:03:48 -04:00
Yi Zhou
f6c5e2928a Add apple-touch-icon link to index.html (#1850) 2026-03-29 12:39:38 -04:00
henrygd
6a207c33fa agent: change disk.Partitions(false) to true - likely fixes empty partition list in docker as of gopsutil 4.26.2 2026-03-29 12:33:45 -04:00
henrygd
9f19afccde hub: reset smart interval on agent reconnect if agent hasn't successfully saved smart devices
this is so people trying to get smart working can see the config changes immediately. not need to wait for the smart interval.
2026-03-29 12:30:39 -04:00
henrygd
f25f2469e3 hub: add debug logs for smart behavior (#1800) 2026-03-28 21:16:26 -04:00
henrygd
5bd43ed461 hub: reset smart interval on agent reconnect if agent hasn't successfully saved smart devices
this is so people trying to get smart working can see the config changes immediately. not need to wait for the smart interval.
2026-03-28 20:47:16 -04:00
henrygd
afdc3f7779 fix(agent): allow GPU_COLLECTOR=nvml without nvidia-smi (#1849) 2026-03-28 18:58:16 -04:00
henrygd
a227c77526 agent: detect podman correctly when using socket proxy (#1846) 2026-03-28 17:43:29 -04:00
henrygd
8202d746af fix(hub): ui bug where charts didn't display 1m max until next update 2026-03-28 12:16:12 -04:00
25 changed files with 746 additions and 261 deletions

View File

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

View File

@@ -1,6 +1,7 @@
package agent
import (
"context"
"log/slog"
"os"
"path/filepath"
@@ -273,7 +274,7 @@ func (a *Agent) initializeDiskInfo() {
hasRoot := false
isWindows := runtime.GOOS == "windows"
partitions, err := disk.Partitions(false)
partitions, err := disk.PartitionsWithContext(context.Background(), true)
if err != nil {
slog.Error("Error getting disk partitions", "err", err)
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/blang/semver"
)
@@ -52,20 +53,22 @@ const (
)
type dockerManager struct {
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList []*container.ApiInfo // List of containers from Docker API
containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
agent *Agent // Used to propagate system detail changes back to the agent
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList []*container.ApiInfo // List of containers from Docker API
containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
dockerVersionChecked bool // Whether a version probe has completed successfully
isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
// Maps cache time intervals to container-specific CPU usage tracking
@@ -78,7 +81,6 @@ type dockerManager struct {
networkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
networkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
lastNetworkReadTime map[uint16]map[string]time.Time // cacheTimeMs -> containerId -> last network read time
retrySleep func(time.Duration)
}
// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests
@@ -87,6 +89,14 @@ type userAgentRoundTripper struct {
userAgent string
}
// dockerVersionResponse contains the /version fields used for engine checks.
type dockerVersionResponse struct {
Version string `json:"Version"`
Components []struct {
Name string `json:"Name"`
} `json:"Components"`
}
// RoundTrip implements the http.RoundTripper interface
func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", u.userAgent)
@@ -134,7 +144,14 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats,
return nil, err
}
dm.isWindows = strings.Contains(resp.Header.Get("Server"), "windows")
// Detect Podman and Windows from Server header
serverHeader := resp.Header.Get("Server")
if !dm.usingPodman && detectPodmanFromHeader(serverHeader) {
dm.setIsPodman()
}
dm.isWindows = strings.Contains(serverHeader, "windows")
dm.ensureDockerVersionChecked()
containersLength := len(dm.apiContainerList)
@@ -588,7 +605,7 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
}
// Creates a new http client for Docker or Podman API
func newDockerManager() *dockerManager {
func newDockerManager(agent *Agent) *dockerManager {
dockerHost, exists := utils.GetEnv("DOCKER_HOST")
if exists {
// return nil if set to empty string
@@ -654,6 +671,7 @@ func newDockerManager() *dockerManager {
}
manager := &dockerManager{
agent: agent,
client: &http.Client{
Timeout: timeout,
Transport: userAgentTransport,
@@ -671,51 +689,54 @@ func newDockerManager() *dockerManager {
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
retrySleep: time.Sleep,
}
// If using podman, return client
if strings.Contains(dockerHost, "podman") {
manager.usingPodman = true
manager.goodDockerVersion = true
return manager
}
// run version check in goroutine to avoid blocking (server may not be ready and requires retries)
go manager.checkDockerVersion()
// give version check a chance to complete before returning
time.Sleep(50 * time.Millisecond)
// Best-effort startup probe. If the engine is not ready yet, getDockerStats will
// retry after the first successful /containers/json request.
_, _ = manager.checkDockerVersion()
return manager
}
// checkDockerVersion checks Docker version and sets goodDockerVersion if at least 25.0.0.
// Versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch.
func (dm *dockerManager) checkDockerVersion() {
var err error
var resp *http.Response
var versionInfo struct {
Version string `json:"Version"`
func (dm *dockerManager) checkDockerVersion() (bool, error) {
resp, err := dm.client.Get("http://localhost/version")
if err != nil {
return false, err
}
const versionMaxTries = 2
for i := 1; i <= versionMaxTries; i++ {
resp, err = dm.client.Get("http://localhost/version")
if err == nil && resp.StatusCode == http.StatusOK {
break
}
if resp != nil {
resp.Body.Close()
}
if i < versionMaxTries {
slog.Debug("Failed to get Docker version; retrying", "attempt", i, "err", err, "response", resp)
dm.retrySleep(5 * time.Second)
}
if resp.StatusCode != http.StatusOK {
status := resp.Status
resp.Body.Close()
return false, fmt.Errorf("docker version request failed: %s", status)
}
if err != nil || resp.StatusCode != http.StatusOK {
var versionInfo dockerVersionResponse
serverHeader := resp.Header.Get("Server")
if err := dm.decode(resp, &versionInfo); err != nil {
return false, err
}
dm.applyDockerVersionInfo(serverHeader, &versionInfo)
dm.dockerVersionChecked = true
return true, nil
}
// ensureDockerVersionChecked retries the version probe after a successful
// container list request.
func (dm *dockerManager) ensureDockerVersionChecked() {
if dm.dockerVersionChecked {
return
}
if err := dm.decode(resp, &versionInfo); err != nil {
if _, err := dm.checkDockerVersion(); err != nil {
slog.Debug("Failed to get Docker version", "err", err)
}
}
// applyDockerVersionInfo updates version-dependent behavior from engine metadata.
func (dm *dockerManager) applyDockerVersionInfo(serverHeader string, versionInfo *dockerVersionResponse) {
if detectPodmanEngine(serverHeader, versionInfo) {
dm.setIsPodman()
return
}
// if version > 24, one-shot works correctly and we can limit concurrent operations
@@ -941,3 +962,46 @@ func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {
func (dm *dockerManager) IsPodman() bool {
return dm.usingPodman
}
// setIsPodman sets the manager to Podman mode and updates system details accordingly.
func (dm *dockerManager) setIsPodman() {
if dm.usingPodman {
return
}
dm.usingPodman = true
dm.goodDockerVersion = true
dm.dockerVersionChecked = true
// keep system details updated - this may be detected late if server isn't ready when
// agent starts, so make sure we notify the hub if this happens later.
if dm.agent != nil {
dm.agent.updateSystemDetails(func(details *system.Details) {
details.Podman = true
})
}
}
// detectPodmanFromHeader identifies Podman from the Docker API server header.
func detectPodmanFromHeader(server string) bool {
return strings.HasPrefix(server, "Libpod")
}
// detectPodmanFromVersion identifies Podman from the version payload.
func detectPodmanFromVersion(versionInfo *dockerVersionResponse) bool {
if versionInfo == nil {
return false
}
for _, component := range versionInfo.Components {
if strings.HasPrefix(component.Name, "Podman") {
return true
}
}
return false
}
// detectPodmanEngine checks both header and version metadata for Podman.
func detectPodmanEngine(serverHeader string, versionInfo *dockerVersionResponse) bool {
if detectPodmanFromHeader(serverHeader) {
return true
}
return detectPodmanFromVersion(versionInfo)
}

View File

@@ -539,59 +539,53 @@ func TestDockerManagerCreation(t *testing.T) {
func TestCheckDockerVersion(t *testing.T) {
tests := []struct {
name string
responses []struct {
statusCode int
body string
}
expectedGood bool
expectedRequests int
name string
statusCode int
body string
server string
expectSuccess bool
expectedGood bool
expectedPodman bool
expectError bool
expectedRequest string
}{
{
name: "200 with good version on first try",
responses: []struct {
statusCode int
body string
}{
{http.StatusOK, `{"Version":"25.0.1"}`},
},
expectedGood: true,
expectedRequests: 1,
name: "good docker version",
statusCode: http.StatusOK,
body: `{"Version":"25.0.1"}`,
expectSuccess: true,
expectedGood: true,
expectedPodman: false,
expectedRequest: "/version",
},
{
name: "200 with old version on first try",
responses: []struct {
statusCode int
body string
}{
{http.StatusOK, `{"Version":"24.0.7"}`},
},
expectedGood: false,
expectedRequests: 1,
name: "old docker version",
statusCode: http.StatusOK,
body: `{"Version":"24.0.7"}`,
expectSuccess: true,
expectedGood: false,
expectedPodman: false,
expectedRequest: "/version",
},
{
name: "non-200 then 200 with good version",
responses: []struct {
statusCode int
body string
}{
{http.StatusServiceUnavailable, `"not ready"`},
{http.StatusOK, `{"Version":"25.1.0"}`},
},
expectedGood: true,
expectedRequests: 2,
name: "podman from server header",
statusCode: http.StatusOK,
body: `{"Version":"5.5.0"}`,
server: "Libpod/5.5.0",
expectSuccess: true,
expectedGood: true,
expectedPodman: true,
expectedRequest: "/version",
},
{
name: "non-200 on all retries",
responses: []struct {
statusCode int
body string
}{
{http.StatusInternalServerError, `"error"`},
{http.StatusUnauthorized, `"error"`},
},
expectedGood: false,
expectedRequests: 2,
name: "non-200 response",
statusCode: http.StatusServiceUnavailable,
body: `"not ready"`,
expectSuccess: false,
expectedGood: false,
expectedPodman: false,
expectError: true,
expectedRequest: "/version",
},
}
@@ -599,13 +593,13 @@ func TestCheckDockerVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
idx := requestCount
requestCount++
if idx >= len(tt.responses) {
idx = len(tt.responses) - 1
assert.Equal(t, tt.expectedRequest, r.URL.EscapedPath())
if tt.server != "" {
w.Header().Set("Server", tt.server)
}
w.WriteHeader(tt.responses[idx].statusCode)
fmt.Fprint(w, tt.responses[idx].body)
w.WriteHeader(tt.statusCode)
fmt.Fprint(w, tt.body)
}))
defer server.Close()
@@ -617,17 +611,24 @@ func TestCheckDockerVersion(t *testing.T) {
},
},
},
retrySleep: func(time.Duration) {},
}
dm.checkDockerVersion()
success, err := dm.checkDockerVersion()
assert.Equal(t, tt.expectSuccess, success)
assert.Equal(t, tt.expectSuccess, dm.dockerVersionChecked)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedRequests, requestCount)
assert.Equal(t, tt.expectedPodman, dm.usingPodman)
assert.Equal(t, 1, requestCount)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
t.Run("request error on all retries", func(t *testing.T) {
t.Run("request error", func(t *testing.T) {
requestCount := 0
dm := &dockerManager{
client: &http.Client{
@@ -638,16 +639,171 @@ func TestCheckDockerVersion(t *testing.T) {
},
},
},
retrySleep: func(time.Duration) {},
}
dm.checkDockerVersion()
success, err := dm.checkDockerVersion()
assert.False(t, success)
require.Error(t, err)
assert.False(t, dm.dockerVersionChecked)
assert.False(t, dm.goodDockerVersion)
assert.Equal(t, 2, requestCount)
assert.False(t, dm.usingPodman)
assert.Equal(t, 1, requestCount)
})
}
// newDockerManagerForVersionTest creates a dockerManager wired to a test server.
func newDockerManagerForVersionTest(server *httptest.Server) *dockerManager {
return &dockerManager{
client: &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
return net.Dial(network, server.Listener.Addr().String())
},
},
},
containerStatsMap: make(map[string]*container.Stats),
lastCpuContainer: make(map[uint16]map[string]uint64),
lastCpuSystem: make(map[uint16]map[string]uint64),
lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
lastNetworkReadTime: make(map[uint16]map[string]time.Time),
}
}
func TestGetDockerStatsChecksDockerVersionAfterContainerList(t *testing.T) {
tests := []struct {
name string
containerServer string
versionServer string
versionBody string
expectedGood bool
expectedPodman bool
}{
{
name: "200 with good version on first try",
versionBody: `{"Version":"25.0.1"}`,
expectedGood: true,
expectedPodman: false,
},
{
name: "200 with old version on first try",
versionBody: `{"Version":"24.0.7"}`,
expectedGood: false,
expectedPodman: false,
},
{
name: "podman detected from server header",
containerServer: "Libpod/5.5.0",
expectedGood: true,
expectedPodman: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestCounts := map[string]int{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCounts[r.URL.EscapedPath()]++
switch r.URL.EscapedPath() {
case "/containers/json":
if tt.containerServer != "" {
w.Header().Set("Server", tt.containerServer)
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[]`)
case "/version":
if tt.versionServer != "" {
w.Header().Set("Server", tt.versionServer)
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, tt.versionBody)
default:
t.Fatalf("unexpected path: %s", r.URL.EscapedPath())
}
}))
defer server.Close()
dm := newDockerManagerForVersionTest(server)
stats, err := dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.True(t, dm.dockerVersionChecked)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedPodman, dm.usingPodman)
assert.Equal(t, 1, requestCounts["/containers/json"])
if tt.expectedPodman {
assert.Equal(t, 0, requestCounts["/version"])
} else {
assert.Equal(t, 1, requestCounts["/version"])
}
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedPodman, dm.usingPodman)
assert.Equal(t, 2, requestCounts["/containers/json"])
if tt.expectedPodman {
assert.Equal(t, 0, requestCounts["/version"])
} else {
assert.Equal(t, 1, requestCounts["/version"])
}
})
}
}
func TestGetDockerStatsRetriesVersionCheckUntilSuccess(t *testing.T) {
requestCounts := map[string]int{}
versionStatuses := []int{http.StatusServiceUnavailable, http.StatusOK}
versionBodies := []string{`"not ready"`, `{"Version":"25.1.0"}`}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCounts[r.URL.EscapedPath()]++
switch r.URL.EscapedPath() {
case "/containers/json":
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[]`)
case "/version":
idx := requestCounts["/version"] - 1
if idx >= len(versionStatuses) {
idx = len(versionStatuses) - 1
}
w.WriteHeader(versionStatuses[idx])
fmt.Fprint(w, versionBodies[idx])
default:
t.Fatalf("unexpected path: %s", r.URL.EscapedPath())
}
}))
defer server.Close()
dm := newDockerManagerForVersionTest(server)
stats, err := dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.False(t, dm.dockerVersionChecked)
assert.False(t, dm.goodDockerVersion)
assert.Equal(t, 1, requestCounts["/version"])
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.True(t, dm.dockerVersionChecked)
assert.True(t, dm.goodDockerVersion)
assert.Equal(t, 2, requestCounts["/containers/json"])
assert.Equal(t, 2, requestCounts["/version"])
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
assert.Empty(t, stats)
assert.Equal(t, 3, requestCounts["/containers/json"])
assert.Equal(t, 2, requestCounts["/version"])
}
func TestCycleCpuDeltas(t *testing.T) {
dm := &dockerManager{
lastCpuContainer: map[uint16]map[string]uint64{

View File

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

View File

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

View File

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

View File

@@ -115,6 +115,26 @@ func (a *Agent) refreshSystemDetails() {
}
}
// attachSystemDetails returns details only for fresh default-interval responses.
func (a *Agent) attachSystemDetails(data *system.CombinedData, cacheTimeMs uint16, includeRequested bool) *system.CombinedData {
if cacheTimeMs != defaultDataCacheTimeMs || (!includeRequested && !a.detailsDirty) {
return data
}
// copy data to avoid adding details to the original cached struct
response := *data
response.Details = &a.systemDetails
a.detailsDirty = false
return &response
}
// updateSystemDetails applies a mutation to the static details payload and marks
// it for inclusion on the next fresh default-interval response.
func (a *Agent) updateSystemDetails(updateFunc func(details *system.Details)) {
updateFunc(&a.systemDetails)
a.detailsDirty = true
}
// Returns current info, stats about the host system
func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
var systemStats system.Stats

61
agent/system_test.go Normal file
View File

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

View File

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

View File

@@ -145,6 +145,7 @@ func (sys *System) update() error {
// update smart interval if it's set on the agent side
if data.Details.SmartInterval > 0 {
sys.smartInterval = data.Details.SmartInterval
sys.manager.hub.Logger().Info("SMART interval updated from agent details", "system", sys.Id, "interval", sys.smartInterval.String())
// make sure we reset expiration of lastFetch to remain as long as the new smart interval
// to prevent premature expiration leading to new fetch if interval is different.
sys.manager.smartFetchMap.UpdateExpiration(sys.Id, sys.smartInterval+time.Minute)
@@ -156,11 +157,10 @@ func (sys *System) update() error {
if sys.smartInterval <= 0 {
sys.smartInterval = time.Hour
}
lastFetch, _ := sys.manager.smartFetchMap.GetOk(sys.Id)
if time.Since(time.UnixMilli(lastFetch-1e4)) >= sys.smartInterval && sys.smartFetching.CompareAndSwap(false, true) {
if sys.shouldFetchSmart() && sys.smartFetching.CompareAndSwap(false, true) {
sys.manager.hub.Logger().Info("SMART fetch", "system", sys.Id, "interval", sys.smartInterval.String())
go func() {
defer sys.smartFetching.Store(false)
sys.manager.smartFetchMap.Set(sys.Id, time.Now().UnixMilli(), sys.smartInterval+time.Minute)
_ = sys.FetchAndSaveSmartDevices()
}()
}
@@ -643,6 +643,7 @@ func (s *System) createSSHClient() error {
return err
}
s.agentVersion, _ = extractAgentVersion(string(s.client.Conn.ServerVersion()))
s.manager.resetFailedSmartFetchState(s.Id)
return nil
}

View File

@@ -41,10 +41,10 @@ var errSystemExists = errors.New("system exists")
// SystemManager manages a collection of monitored systems and their connections.
// It handles system lifecycle, status updates, and maintains both SSH and WebSocket connections.
type SystemManager struct {
hub hubLike // Hub interface for database and alert operations
systems *store.Store[string, *System] // Thread-safe store of active systems
sshConfig *ssh.ClientConfig // SSH client configuration for system connections
smartFetchMap *expirymap.ExpiryMap[int64] // Stores last SMART fetch time per system ID
hub hubLike // Hub interface for database and alert operations
systems *store.Store[string, *System] // Thread-safe store of active systems
sshConfig *ssh.ClientConfig // SSH client configuration for system connections
smartFetchMap *expirymap.ExpiryMap[smartFetchState] // Stores last SMART fetch time/result; TTL is only for cleanup
}
// hubLike defines the interface requirements for the hub dependency.
@@ -62,7 +62,7 @@ func NewSystemManager(hub hubLike) *SystemManager {
return &SystemManager{
systems: store.New(map[string]*System{}),
hub: hub,
smartFetchMap: expirymap.New[int64](time.Hour),
smartFetchMap: expirymap.New[smartFetchState](time.Hour),
}
}
@@ -306,6 +306,7 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
if err != nil {
return err
}
sm.resetFailedSmartFetchState(systemId)
system := sm.NewSystem(systemId)
system.WsConn = wsConn
@@ -317,6 +318,15 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
return nil
}
// resetFailedSmartFetchState clears only failed SMART cooldown entries so a fresh
// agent reconnect retries SMART discovery immediately after configuration changes.
func (sm *SystemManager) resetFailedSmartFetchState(systemID string) {
state, ok := sm.smartFetchMap.GetOk(systemID)
if ok && !state.Successful {
sm.smartFetchMap.Remove(systemID)
}
}
// createSSHClientConfig initializes the SSH client configuration for connecting to an agent's server
func (sm *SystemManager) createSSHClientConfig() error {
privateKey, err := sm.hub.GetSSHKey("")

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.18.5",
"version": "0.18.6",
"type": "module",
"scripts": {
"dev": "vite --host",

View File

@@ -67,8 +67,8 @@ export default function AreaChartDefault({
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
const sourceData = customData ?? chartData.systemStats
// Only update the rendered data while the chart is visible
const [displayData, setDisplayData] = useState(sourceData)
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
// Reduce chart redraws by only updating while visible or when chart time changes
useEffect(() => {
@@ -78,7 +78,10 @@ export default function AreaChartDefault({
if (shouldUpdate) {
setDisplayData(sourceData)
}
}, [displayData, isIntersecting, sourceData])
if (isIntersecting && maxToggled !== displayMaxToggled) {
setDisplayMaxToggled(maxToggled)
}
}, [displayData, displayMaxToggled, isIntersecting, maxToggled, sourceData])
// Use a stable key derived from data point identities and visual properties
const areasKey = dataPoints?.map((d) => `${d.label}:${d.opacity}`).join("\0")
@@ -106,14 +109,14 @@ export default function AreaChartDefault({
/>
)
})
}, [areasKey, maxToggled])
}, [areasKey, displayMaxToggled])
return useMemo(() => {
if (displayData.length === 0) {
return null
}
// if (logRender) {
// console.log("Rendered at", new Date(), "for", dataPoints?.at(0)?.label)
// console.log("Rendered", dataPoints?.map((d) => d.label).join(", "), new Date())
// }
return (
<ChartContainer
@@ -163,5 +166,5 @@ export default function AreaChartDefault({
</AreaChart>
</ChartContainer>
)
}, [displayData, yAxisWidth, showTotal, filter])
}, [displayData, yAxisWidth, filter, Areas])
}

View File

@@ -66,8 +66,8 @@ export default function LineChartDefault({
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
const sourceData = customData ?? chartData.systemStats
// Only update the rendered data while the chart is visible
const [displayData, setDisplayData] = useState(sourceData)
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
// Reduce chart redraws by only updating while visible or when chart time changes
useEffect(() => {
@@ -77,7 +77,10 @@ export default function LineChartDefault({
if (shouldUpdate) {
setDisplayData(sourceData)
}
}, [displayData, isIntersecting, sourceData])
if (isIntersecting && maxToggled !== displayMaxToggled) {
setDisplayMaxToggled(maxToggled)
}
}, [displayData, displayMaxToggled, isIntersecting, maxToggled, sourceData])
// Use a stable key derived from data point identities and visual properties
const linesKey = dataPoints?.map((d) => `${d.label}:${d.strokeOpacity ?? ""}`).join("\0")
@@ -105,14 +108,14 @@ export default function LineChartDefault({
/>
)
})
}, [linesKey, maxToggled])
}, [linesKey, displayMaxToggled])
return useMemo(() => {
if (displayData.length === 0) {
return null
}
// if (logRender) {
// console.log("Rendered at", new Date(), "for", dataPoints?.at(0)?.label)
// console.log("Rendered", dataPoints?.map((d) => d.label).join(", "), new Date())
// }
return (
<ChartContainer
@@ -162,5 +165,5 @@ export default function LineChartDefault({
</LineChart>
</ChartContainer>
)
}, [displayData, yAxisWidth, showTotal, filter, chartData.chartTime])
}, [displayData, yAxisWidth, filter, Lines])
}

View File

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

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-28 09:32\n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "Версия {0} е налична"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -476,7 +476,7 @@ msgstr "Копирай YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Основни"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +550,7 @@ msgstr "Дневно"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Подредба"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -611,7 +611,7 @@ msgstr "Изполване на диск от {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Показване"
msgstr ""
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -845,7 +845,7 @@ msgstr "Глобален"
#: src/components/routes/system.tsx
msgid "GPU"
msgstr "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines"
@@ -870,7 +870,7 @@ msgstr "Здраве"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -1259,7 +1259,7 @@ msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Портове"
msgstr ""
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1549,7 +1549,7 @@ msgstr "Таблица"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Табове"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1879,4 +1879,3 @@ msgstr "Да"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Настройките за потребителя ти са обновени."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-31 07:42\n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} tilgjengelig"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -57,7 +57,7 @@ msgstr "1 time"
#. Load average
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min"
msgstr "1 min"
msgstr ""
#: src/lib/utils.ts
msgid "1 minute"
@@ -74,7 +74,7 @@ msgstr "12 timer"
#. Load average
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min"
msgstr "15 min"
msgstr ""
#: src/lib/utils.ts
msgid "24 hours"
@@ -87,7 +87,7 @@ msgstr "30 dager"
#. Load average
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min"
msgstr "5 min"
msgstr ""
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
@@ -137,7 +137,7 @@ msgstr "Juster bredden på hovedlayouten"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin"
msgstr "Admin"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "After"
@@ -149,7 +149,7 @@ msgstr "Etter å ha angitt miljøvariablene, start Beszel-huben på nytt for at
#: 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
@@ -280,7 +280,7 @@ msgstr "Binær"
#: 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"
@@ -289,7 +289,7 @@ msgstr "Oppstartstilstand"
#: 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/routes/system/charts/memory-charts.tsx
msgid "Cache / Buffers"
@@ -327,7 +327,7 @@ msgstr "Advarsel - potensielt tap av data"
#: 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."
@@ -476,13 +476,13 @@ msgstr "Kopier YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Kjerne"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr "CPU"
msgstr ""
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
@@ -550,7 +550,7 @@ msgstr "Daglig"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Standard"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -587,11 +587,11 @@ msgstr "Lader ut"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Disk"
msgstr "Disk"
msgstr ""
#: src/components/routes/system/charts/disk-charts.tsx
msgid "Disk I/O"
msgstr "Disk I/O"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
@@ -611,7 +611,7 @@ msgstr "Diskbruk av {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Vis"
msgstr ""
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -759,7 +759,7 @@ msgstr "Eksporter din nåværende systemkonfigurasjon"
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
@@ -803,7 +803,7 @@ msgstr "Mislyktes: {0}"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Filter..."
msgstr "Filter..."
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
@@ -841,11 +841,11 @@ msgstr "Generelt"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Global"
msgstr ""
#: src/components/routes/system.tsx
msgid "GPU"
msgstr "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines"
@@ -870,7 +870,7 @@ msgstr "Helse"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Hjerteslag"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -910,7 +910,7 @@ msgstr "Dersom du har mistet passordet til admin-kontoen kan du nullstille det m
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Docker image"
msgid "Image"
msgstr "Image"
msgstr ""
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
@@ -1188,7 +1188,7 @@ msgstr "Fortid"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pause"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Paused"
@@ -1213,7 +1213,7 @@ msgstr "Prosentandel av tid brukt i hver tilstand"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Permanent"
msgstr "Permanent"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Persistence"
@@ -1254,12 +1254,12 @@ msgstr "Vennligst logg inn på kontoen din"
#: src/components/add-system.tsx
msgid "Port"
msgstr "Port"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Porter"
msgstr ""
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1497,7 +1497,7 @@ msgstr "Tilstand"
#: 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"
@@ -1524,7 +1524,7 @@ msgstr "Swap-bruk"
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
msgstr "System"
msgstr ""
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time"
@@ -1549,7 +1549,7 @@ msgstr "Tabell"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Faner"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1559,7 +1559,7 @@ msgstr "Oppgaver"
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Temp"
msgstr "Temp"
msgstr ""
#: src/components/routes/system/charts/sensor-charts.tsx
#: src/lib/alerts.ts
@@ -1576,7 +1576,7 @@ msgstr "Temperaturer på system-sensorer"
#: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
@@ -1626,7 +1626,7 @@ msgstr "Tema av/på"
#: 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
@@ -1716,7 +1716,7 @@ msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grensever
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Type"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
@@ -1730,7 +1730,7 @@ msgstr "Enhetspreferanser"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Universal token"
msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
@@ -1879,4 +1879,3 @@ msgstr "Ja"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Dine brukerinnstillinger har blitt oppdatert."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-29 01:19\n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} jest dostępna"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -57,7 +57,7 @@ msgstr "1 godzina"
#. Load average
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "1 min"
msgstr "1 min"
msgstr ""
#: src/lib/utils.ts
msgid "1 minute"
@@ -74,7 +74,7 @@ msgstr "12 godzin"
#. Load average
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "15 min"
msgstr "15 min"
msgstr ""
#: src/lib/utils.ts
msgid "24 hours"
@@ -87,7 +87,7 @@ msgstr "30 dni"
#. Load average
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "5 min"
msgstr "5 min"
msgstr ""
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
@@ -137,7 +137,7 @@ msgstr "Dostosuj szerokość widoku"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Admin"
msgstr "Admin"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "After"
@@ -145,11 +145,11 @@ msgstr "Po"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "Po ustawieniu zmiennych środowiskowych zrestartuj Beszel hub, aby zmiany zostały zaaplikowane."
msgstr "Po ustawieniu zmiennych środowiskowych zrestartuj hub Beszel, aby zmiany weszły w życie."
#: 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
@@ -476,7 +476,7 @@ msgstr "Kopiuj YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Główne"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +550,7 @@ msgstr "Codziennie"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "Domyślne"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -611,7 +611,7 @@ msgstr "Wykorzystanie dysku {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Widok"
msgstr ""
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -759,7 +759,7 @@ msgstr "Eksportuj aktualną konfigurację systemów."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
@@ -845,7 +845,7 @@ msgstr "Globalny"
#: src/components/routes/system.tsx
msgid "GPU"
msgstr "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines"
@@ -870,7 +870,7 @@ msgstr "Kondycja"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -944,7 +944,7 @@ msgstr "Cykl życia"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "limit"
msgstr "limit"
msgstr ""
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "Load Average"
@@ -1046,7 +1046,7 @@ msgstr "Użycie pamięci przez kontenery Docker."
#. Device model
#: src/components/routes/system/smart-table.tsx
msgid "Model"
msgstr "Model"
msgstr ""
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
@@ -1200,7 +1200,7 @@ msgstr "Wstrzymane ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Format payload'u"
msgstr "Format ładunku"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
@@ -1254,12 +1254,12 @@ msgstr "Zaloguj się na swoje konto"
#: src/components/add-system.tsx
msgid "Port"
msgstr "Port"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Porty"
msgstr ""
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1345,7 +1345,7 @@ msgstr "Wznów"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr "Root"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1422,7 +1422,7 @@ msgstr "Wyślij pojedynczy ping heartbeat, aby sprawdzić, czy punkt końcowy dz
#: src/components/routes/settings/heartbeat.tsx
msgid "Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet."
msgstr "Wysyłaj cyklicznie pingi wychodzące do zewnętrznej usługi monitorowania, co pozwala monitorować Beszel bez potrzeby udostępniania go publicznie w sieci."
msgstr "Wysyłaj okresowe pingi wychodzące do zewnętrznej usługi monitorowania, dzięki czemu możesz monitorować Beszel bez wystawiania go na działanie Internetu."
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
@@ -1497,7 +1497,7 @@ msgstr "Stan"
#: 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"
@@ -1524,7 +1524,7 @@ msgstr "Użycie pamięci wymiany"
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
msgstr "System"
msgstr ""
#: src/components/routes/system/charts/load-average-chart.tsx
msgid "System load averages over time"
@@ -1549,7 +1549,7 @@ msgstr "Tabela"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Karty"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1576,7 +1576,7 @@ msgstr "Temperatury czujników systemowych."
#: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
@@ -1588,7 +1588,7 @@ msgstr "Testowe powiadomienie wysłane."
#: src/components/routes/settings/heartbeat.tsx
msgid "The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down."
msgstr "Ogólny status to <0>ok</0>, gdy wszystkie systemy działają, <1>ostrzeżenie</1>, gdy wyzwalane są alerty, oraz <2>błąd</2>, gdy którykolwiek z systemów nie działa."
msgstr "Ogólny status to <0>ok</0>, gdy wszystkie systemy działają, <1>ostrzeżenie</1>, gdy wyzwalane są alerty, oraz <2>błąd</2>, gdy którykolwiek system nie działa."
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
@@ -1626,7 +1626,7 @@ msgstr "Zmień motyw"
#: 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
@@ -1848,7 +1848,7 @@ msgstr "Gdy jest włączony, ten token pozwala agentom na samodzielną rejestrac
#: src/components/routes/settings/heartbeat.tsx
msgid "When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts."
msgstr "Przy użyciu metody POST każdy heartbeat zawiera payload JSON z podsumowaniem statusu systemu, listą niedostępnych systemów i wywołanymi alertami."
msgstr "W przypadku korzystania z POST każdy heartbeat zawiera ładunek JSON z podsumowaniem statusu systemu, listą wyłączonych systemów i wyzwolonymi alertami."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -1879,4 +1879,3 @@ msgstr "Tak"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-27 22:12\n"
"PO-Revision-Date: 2026-02-21 09:46\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} доступно"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -476,7 +476,7 @@ msgstr "Скопировать YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "Процессор"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
@@ -550,7 +550,7 @@ msgstr "Ежедневно"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "По умолчанию"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -611,7 +611,7 @@ msgstr "Использование диска {extraFsName}"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "Отображение"
msgstr ""
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -845,7 +845,7 @@ msgstr "Глобально"
#: src/components/routes/system.tsx
msgid "GPU"
msgstr "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines"
@@ -870,7 +870,7 @@ msgstr "Здоровье"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -1259,7 +1259,7 @@ msgstr "Порт"
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Порты"
msgstr ""
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1549,7 +1549,7 @@ msgstr "Таблица"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Вкладки"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1879,4 +1879,3 @@ msgstr "Да"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Ваши настройки пользователя были обновлены."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-03-28 02:52\n"
"PO-Revision-Date: 2026-03-27 19:17\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -22,7 +22,7 @@ msgstr ""
#: src/components/footer-repo-link.tsx
msgctxt "New version available"
msgid "{0} available"
msgstr "{0} 現已推出"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
@@ -145,11 +145,11 @@ msgstr "之後"
#: src/components/routes/settings/heartbeat.tsx
msgid "After setting the environment variables, restart your Beszel hub for changes to take effect."
msgstr "設環境變數後,重新啟動 Beszel Hub 以使更生效。"
msgstr "設環境變數後,重新啟動您的 Beszel hub 以使更生效。"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
msgstr "Agent"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -476,13 +476,13 @@ msgstr "複製 YAML"
#: src/components/routes/system.tsx
msgctxt "Core system metrics"
msgid "Core"
msgstr "核心指標"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr "CPU"
msgstr ""
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
@@ -550,7 +550,7 @@ msgstr "每日"
#: src/components/routes/system/info-bar.tsx
msgctxt "Default system layout option"
msgid "Default"
msgstr "預設"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -611,7 +611,7 @@ msgstr "{extraFsName}的磁碟使用量"
#: src/components/routes/system/info-bar.tsx
msgctxt "Layout display options"
msgid "Display"
msgstr "顯示"
msgstr ""
#: src/components/routes/system/charts/cpu-charts.tsx
msgid "Docker CPU Usage"
@@ -682,11 +682,11 @@ msgstr "結束時間"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL"
msgstr "Endpoint URL"
msgstr "端點 URL"
#: src/components/routes/settings/heartbeat.tsx
msgid "Endpoint URL to ping (required)"
msgstr "要 Ping 的 Endpoint URL (必填)"
msgstr "要 ping 的端點 URL (必填)"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
@@ -781,7 +781,7 @@ msgstr "儲存設定失敗"
#: src/components/routes/settings/heartbeat.tsx
msgid "Failed to send heartbeat"
msgstr "Heartbeat 發送失敗"
msgstr "發送 heartbeat 失敗"
#: src/components/routes/settings/notifications.tsx
msgid "Failed to send test notification"
@@ -807,7 +807,7 @@ msgstr "篩選..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr "Fingerprint"
msgstr ""
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
@@ -845,7 +845,7 @@ msgstr "全域"
#: src/components/routes/system.tsx
msgid "GPU"
msgstr "GPU"
msgstr ""
#: src/components/routes/system/charts/gpu-charts.tsx
msgid "GPU Engines"
@@ -870,7 +870,7 @@ msgstr "健康狀態"
#: src/components/routes/settings/layout.tsx
msgid "Heartbeat"
msgstr "Heartbeat"
msgstr "心跳 (Heartbeat)"
#: src/components/routes/settings/heartbeat.tsx
msgid "Heartbeat Monitoring"
@@ -888,7 +888,7 @@ msgstr "Homebrew 指令"
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr "Host / IP"
msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "HTTP Method"
@@ -1200,7 +1200,7 @@ msgstr "已暫停 ({pausedSystemsLength})"
#: src/components/routes/settings/heartbeat.tsx
msgid "Payload format"
msgstr "Payload 格式"
msgstr "有效載荷 (Payload) 格式"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
@@ -1254,12 +1254,12 @@ msgstr "請登入您的帳號"
#: src/components/add-system.tsx
msgid "Port"
msgstr "Port"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Container ports"
msgid "Ports"
msgstr "Port 映射"
msgstr ""
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1345,7 +1345,7 @@ msgstr "繼續"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr "Root"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1418,15 +1418,15 @@ msgstr "選取{foo}"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send a single heartbeat ping to verify your endpoint is working."
msgstr "發送單次 Heartbeat Ping 以驗證您的 Endpoint 是否運作正常。"
msgstr "發送單個 heartbeat ping 以驗證您的端點是否正常工作。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet."
msgstr "定期發送 Outbound Ping 至外部監控服務,讓您無需將 Beszel 暴露於網際網路即可進行監控。"
msgstr "定期向外部監控服務發送出站 ping以便您在不將 Beszel 暴露於網際網路的情況下進行監控。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Send test heartbeat"
msgstr "發送測試 Heartbeat"
msgstr "發送測試 heartbeat"
#: src/components/routes/system/charts/network-charts.tsx
msgid "Sent"
@@ -1451,7 +1451,7 @@ msgstr "設定儀表顏色的百分比閾值。"
#: src/components/routes/settings/heartbeat.tsx
msgid "Set the following environment variables on your Beszel hub to enable heartbeat monitoring:"
msgstr "設定以下環境變數至 Beszel Hub 以啟用 Heartbeat 監控:"
msgstr "在您的 Beszel hub 上設置以下環境變數以啟用 heartbeat 監控:"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
@@ -1549,7 +1549,7 @@ msgstr "列表"
#: src/components/routes/system/info-bar.tsx
msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "分頁"
msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Tasks"
@@ -1580,7 +1580,7 @@ msgstr "測試<0>URL</0>"
#: src/components/routes/settings/heartbeat.tsx
msgid "Test heartbeat"
msgstr "測試 Heartbeat"
msgstr "測試 heartbeat"
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
@@ -1588,7 +1588,7 @@ msgstr "已發送測試通知"
#: src/components/routes/settings/heartbeat.tsx
msgid "The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down."
msgstr "當所有系統都正常運行時,整體狀態為 <0>正常</0>;當觸發警報時為 <1>警告</1>;當任何系統故障時為 <2>錯誤</2>。"
msgstr "當所有系統都正常運行時,整體狀態為 <0>ok</0>;當觸發警報時為 <1>警告</1>;當任何系統故障時為 <2>錯誤</2>。"
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
@@ -1626,7 +1626,7 @@ msgstr "切換主題"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1848,7 +1848,7 @@ msgstr "啟用後,此 Token 可讓 Agent 自行註冊,無需預先在系統
#: src/components/routes/settings/heartbeat.tsx
msgid "When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts."
msgstr "使用 POST 時,每個 Heartbeat 都包含一個 JSON Payload內容涵蓋系統狀態概況、離線系統清單以及已觸發的警報。"
msgstr "使用 POST 時,每個 heartbeat 都包含一個 JSON 有效載荷,其中包含系統狀態摘要、故障系統列表和觸發的警報。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -1879,4 +1879,3 @@ msgstr "是"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "已更新您的使用者設定"

View File

@@ -1,3 +1,17 @@
## 0.18.6
- Add apple-touch-icon link to index.html (#1850)
- Fix UI bug where charts did not display 1m max until next update
- Fix regression in partition discovery on Docker (#1847)
- Fix agent detection of Podman when using socket proxy (#1846)
- Fix NVML GPU collection being disabled when `nvidia-smi` is not in PATH (#1849)
- Reset SMART interval on agent reconnect if the agent hasn't collected SMART data, allowing config changes to take effect immediately
## 0.18.5
- Add "update available" notification in hub web UI with `CHECK_UPDATES=true` (#1830)