Compare commits

..

40 Commits

Author SHA1 Message Date
dalton-baker
99c7f7bd8a Add GPU-enabled build target in dockerfile_Agent (nvidia-smi support) (#898) 2025-08-03 13:31:26 -04:00
henrygd
8af3a0eb5b refactor: add getMeterState function 2025-08-02 23:44:07 -04:00
henrygd
5f7950b474 tweaks to custom meter percentages 2025-08-02 20:53:49 -04:00
Sven van Ginkel
df9e2dec28 [Feature] Add custom meter percentages (#942) 2025-08-02 17:58:52 -04:00
henrygd
a0f271545a refactoring (no functionality changes) 2025-08-02 17:04:38 -04:00
Bradley Varol
aa2bc9f118 fix systems table names wrapping (#1027) 2025-08-02 12:28:49 -04:00
henrygd
b22ae87022 disable winget auto pr and reactivate docker workflow 2025-08-01 21:15:37 -04:00
henrygd
79e79079bc fix goreleaser winget token field and temp disable docker workflow 2025-08-01 20:43:33 -04:00
henrygd
1811ebdee4 0.12.2 release :) 2025-08-01 20:36:29 -04:00
henrygd
137f3f3e24 update language files 2025-08-01 20:34:38 -04:00
Mikael Richardsson
ed1d1e77c0 Update Swedish translations 2025-08-01 20:29:02 -04:00
henrygd
8c36dd1caa windows: embed LibreHardwareMonitorLib for better sensors detection
- Updated GitHub Actions release workflow to set up .NET and build the LHM executable.
- Modified Makefile to include a conditional build step for the .NET executable on Windows.
2025-08-01 20:04:40 -04:00
hank
57bfe72486 Update readme.md 2025-07-31 19:43:08 -04:00
henrygd
75f66b0246 fix: handle missing docker group in debian postinstall script (#1012)
Check if docker group exists before attempting to add beszel user to it, preventing installation failure when Docker is not installed.
2025-07-30 19:09:10 -04:00
henrygd
ce93d54aa7 fix agent data directory resolution (#991) 2025-07-30 14:34:36 -04:00
henrygd
39dbe0eac5 ensure /etc/machine-id exists for persistent fingerprint in install-agent.sh 2025-07-30 14:25:41 -04:00
henrygd
7282044f80 improve memo deps for default area chart 2025-07-29 20:53:44 -04:00
henrygd
d77c37c0b0 winget upgrade: make sure service is stopped before updating package 2025-07-29 18:37:34 -04:00
Sven van Ginkel
e362cbbca5 Move copy button (#1010)
Thank you!
2025-07-28 19:20:37 -04:00
evrial
118544926b [Fix] OpenWrt agent install script (#1005)
* Update install-agent.sh

* Update install-agent.sh

* Update install-agent.sh
2025-07-28 14:56:31 -04:00
henrygd
d4bb0a0a30 fix: consolidate OpenWRT environment variables into single procd_set_param call 2025-07-27 18:51:26 -04:00
henrygd
fe5e35d1a9 agent install script: improve openwrt compatibility 2025-07-27 18:44:36 -04:00
henrygd
60a6ae2caa add winget token for goreleaser action 2025-07-25 20:13:31 -04:00
henrygd
80338d36aa release 0.12.1 :) 2025-07-25 19:23:53 -04:00
henrygd
f0d2c242e8 update language files 2025-07-25 19:19:49 -04:00
zoixc
559f83d99c Updated Russian translations 2025-07-25 19:14:08 -04:00
Kamil
d3a751ee6c Updated Polish translations 2025-07-25 19:13:18 -04:00
Sven van Ginkel
fb70a166fa Updated Dutch translations 2025-07-25 19:13:18 -04:00
Alberto Rizzi
c12457b707 Updated Italian translations 2025-07-25 19:08:03 -04:00
Lucas Pedro
3e53d73d56 Updated Portuguese translations 2025-07-25 19:03:45 -04:00
Mikki Alexander Mousing Sørensen
80338c5e98 Updated Danish translations 2025-07-25 19:01:34 -04:00
NickAss512
249cd8ad19 Updated Czech translations
Co-authored-by: Gear <88455107+GearCzech@users.noreply.github.com>
2025-07-25 19:01:34 -04:00
henrygd
ccdff46370 add TOKEN_FILE env var 2025-07-25 18:26:31 -04:00
henrygd
91679b5cc0 refactor load average handling (#982)
- Transitioned from individual load average fields (LoadAvg1, LoadAvg5,
LoadAvg15) to a single array (LoadAvg)
- Ensure load is displayed when all zero values.
2025-07-25 18:07:29 -04:00
henrygd
6953edf59e sort token / fingerprint table by system name 2025-07-25 15:53:40 -04:00
henrygd
b91c77ec92 make sure only 200 records are returned for alert history table 2025-07-25 15:43:19 -04:00
henrygd
3ac0b185d1 fix oidc icon display issue (#990) 2025-07-25 13:55:02 -04:00
henrygd
1e675cabb5 refactor agent data directory resolution (#991) 2025-07-25 13:37:23 -04:00
henrygd
5f44965c2c improve table formatting and fix #983
- should fix NaN display bug for dasboard cpu
- standardize decimals for dash meters
2025-07-25 00:29:08 -04:00
henrygd
f080929296 Update goreleaser configuration
- Removed the name field for brew.
- Enabled pull requests for winget.
2025-07-24 22:31:27 -04:00
77 changed files with 2164 additions and 883 deletions

View File

@@ -21,6 +21,14 @@ jobs:
- image: henrygd/beszel-agent
context: ./beszel
dockerfile: ./beszel/dockerfile_Agent
target: default-agent
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
- image: henrygd/beszel-agent-nvidia
context: ./beszel
dockerfile: ./beszel/dockerfile_Agent
target: nvidia-agent
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
@@ -33,6 +41,14 @@ jobs:
- image: ghcr.io/${{ github.repository }}/beszel-agent
context: ./beszel
dockerfile: ./beszel/dockerfile_Agent
target: default-agent
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
context: ./beszel
dockerfile: ./beszel/dockerfile_Agent
target: nvidia-agent
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
@@ -91,3 +107,4 @@ jobs:
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
target: ${{ matrix.target }}

View File

@@ -3,7 +3,7 @@ name: Make release and binaries
on:
push:
tags:
- 'v*'
- "v*"
permissions:
contents: write
@@ -29,7 +29,17 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '^1.22.1'
go-version: "^1.22.1"
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
- name: Build .NET LHM executable for Windows sensors
run: |
dotnet build -c Release ./beszel/internal/agent/lhm/beszel_lhm.csproj
shell: bash
- name: GoReleaser beszel
uses: goreleaser/goreleaser-action@v6
@@ -40,3 +50,4 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ beszel/build
beszel/site/src/locales/**/*.ts
*.bak
__debug_*
beszel/internal/agent/lhm/obj
beszel/internal/agent/lhm/bin

View File

@@ -173,7 +173,6 @@ brews:
error_log_path "#{Dir.home}/.cache/beszel/beszel-agent.log"
keep_alive true
restart_delay 5
name beszel-agent
process_type :background
winget:
@@ -203,13 +202,14 @@ winget:
owner: henrygd
name: beszel-winget
branch: henrygd.beszel-agent-{{ .Version }}
pull_request:
enabled: false
draft: false
base:
owner: microsoft
name: winget-pkgs
branch: master
token: "{{ .Env.WINGET_TOKEN }}"
# pull_request:
# enabled: true
# draft: false
# base:
# owner: microsoft
# name: winget-pkgs
# branch: master
release:
draft: true

View File

@@ -4,6 +4,9 @@ ARCH ?= $(shell go env GOARCH)
# Skip building the web UI if true
SKIP_WEB ?= false
# Set executable extension based on target OS
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales
.DEFAULT_GOAL := build
@@ -30,11 +33,25 @@ build-web-ui:
npm run --prefix ./site build; \
fi
build-agent: tidy
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/agent
# Conditional .NET build - only for Windows
build-dotnet-conditional:
@if [ "$(OS)" = "windows" ]; then \
echo "Building .NET executable for Windows..."; \
if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./internal/agent/lhm/bin; \
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
else \
echo "Error: dotnet not found. Install .NET SDK to build Windows agent."; \
exit 1; \
fi; \
fi
# Update build-agent to include conditional .NET build
build-agent: tidy build-dotnet-conditional
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/agent
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/hub
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
build: build-agent build-hub
@@ -67,6 +84,15 @@ dev-agent:
else \
go run beszel/cmd/agent; \
fi
build-dotnet:
@if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./internal/agent/lhm/bin; \
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
else \
echo "dotnet not found"; \
fi
# KEY="..." make -j dev
dev: dev-server dev-hub dev-agent

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"log"
"os"
"strings"
"golang.org/x/crypto/ssh"
)
@@ -25,13 +26,16 @@ func (opts *cmdOptions) parse() bool {
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
flag.Usage = func() {
fmt.Printf("Usage: %s [command] [flags]\n", os.Args[0])
fmt.Println("\nCommands:")
fmt.Println(" health Check if the agent is running")
fmt.Println(" help Display this help message")
fmt.Println(" update Update to the latest version")
fmt.Println(" version Display the version")
fmt.Println("\nFlags:")
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\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())
flag.PrintDefaults()
}
@@ -111,12 +115,12 @@ func main() {
serverConfig.Addr = addr
serverConfig.Network = agent.GetNetwork(addr)
agent, err := agent.NewAgent("")
a, err := agent.NewAgent()
if err != nil {
log.Fatal("Failed to create agent: ", err)
}
if err := agent.Start(serverConfig); err != nil {
if err := a.Start(serverConfig); err != nil {
log.Fatal("Failed to start server: ", err)
}
}

View File

@@ -14,13 +14,21 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
RUN rm -rf /tmp/*
# ? -------------------------
FROM scratch
# --------------------------
# Final image: default scratch-based agent
# --------------------------
FROM scratch AS default-agent
COPY --from=builder /agent /agent
# this is so we don't need to create the
# /tmp directory in the scratch container
# this is so we don't need to create the /tmp directory in the scratch container
COPY --from=builder /tmp /tmp
ENTRYPOINT ["/agent"]
# --------------------------
# Final image: GPU-enabled agent with nvidia-smi
# --------------------------
FROM nvidia/cuda:12.9.0-base-ubuntu22.04 AS nvidia-agent
COPY --from=builder /agent /agent
COPY --from=builder /tmp /tmp
ENTRYPOINT ["/agent"]

View File

@@ -40,13 +40,13 @@ type Agent struct {
// NewAgent creates a new agent with the given data directory for persisting data.
// If the data directory is not set, it will attempt to find the optimal directory.
func NewAgent(dataDir string) (agent *Agent, err error) {
func NewAgent(dataDir ...string) (agent *Agent, err error) {
agent = &Agent{
fsStats: make(map[string]*system.FsStats),
cache: NewSessionCache(69 * time.Second),
}
agent.dataDir, err = getDataDir(dataDir)
agent.dataDir, err = getDataDir(dataDir...)
if err != nil {
slog.Warn("Data directory not found")
} else {
@@ -113,37 +113,37 @@ func (a *Agent) gatherStats(sessionID string) *system.CombinedData {
a.Lock()
defer a.Unlock()
cachedData, ok := a.cache.Get(sessionID)
if ok {
slog.Debug("Cached stats", "session", sessionID)
return cachedData
data, isCached := a.cache.Get(sessionID)
if isCached {
slog.Debug("Cached data", "session", sessionID)
return data
}
*cachedData = system.CombinedData{
*data = system.CombinedData{
Stats: a.getSystemStats(),
Info: a.systemInfo,
}
slog.Debug("System stats", "data", cachedData)
slog.Debug("System data", "data", data)
if a.dockerManager != nil {
if containerStats, err := a.dockerManager.getDockerStats(); err == nil {
cachedData.Containers = containerStats
slog.Debug("Docker stats", "data", cachedData.Containers)
data.Containers = containerStats
slog.Debug("Containers", "data", data.Containers)
} else {
slog.Debug("Docker stats", "err", err)
slog.Debug("Containers", "err", err)
}
}
cachedData.Stats.ExtraFs = make(map[string]*system.FsStats)
data.Stats.ExtraFs = make(map[string]*system.FsStats)
for name, stats := range a.fsStats {
if !stats.Root && stats.DiskTotal > 0 {
cachedData.Stats.ExtraFs[name] = stats
data.Stats.ExtraFs[name] = stats
}
}
slog.Debug("Extra filesystems", "data", cachedData.Stats.ExtraFs)
slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
a.cache.Set(sessionID, cachedData)
return cachedData
a.cache.Set(sessionID, data)
return data
}
// StartAgent initializes and starts the agent with optional WebSocket connection

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
@@ -53,9 +54,9 @@ func newWebSocketClient(agent *Agent) (client *WebSocketClient, err error) {
return nil, errors.New("invalid hub URL")
}
// get registration token
client.token, _ = GetEnv("TOKEN")
if client.token == "" {
return nil, errors.New("TOKEN environment variable not set")
client.token, err = getToken()
if err != nil {
return nil, err
}
client.agent = agent
@@ -65,6 +66,27 @@ func newWebSocketClient(agent *Agent) (client *WebSocketClient, err error) {
return client, nil
}
// getToken returns the token for the WebSocket client.
// It first checks the TOKEN environment variable, then the TOKEN_FILE environment variable.
// If neither is set, it returns an error.
func getToken() (string, error) {
// get token from env var
token, _ := GetEnv("TOKEN")
if token != "" {
return token, nil
}
// get token from file
tokenFile, _ := GetEnv("TOKEN_FILE")
if tokenFile == "" {
return "", errors.New("must set TOKEN or TOKEN_FILE")
}
tokenBytes, err := os.ReadFile(tokenFile)
if err != nil {
return "", err
}
return string(tokenBytes), nil
}
// getOptions returns the WebSocket client options, creating them if necessary.
// It configures the connection URL, TLS settings, and authentication headers.
func (client *WebSocketClient) getOptions() *gws.ClientOption {

View File

@@ -61,7 +61,7 @@ func TestNewWebSocketClient(t *testing.T) {
hubURL: "http://localhost:8080",
token: "",
expectError: true,
errorMsg: "TOKEN environment variable not set",
errorMsg: "must set TOKEN or TOKEN_FILE",
},
}
@@ -389,3 +389,150 @@ func TestWebSocketClient_ConnectRateLimit(t *testing.T) {
err = client.Connect()
assert.Error(t, err, "Connection should fail but not hang")
}
// TestGetToken tests the getToken function with various scenarios
func TestGetToken(t *testing.T) {
unsetEnvVars := func() {
os.Unsetenv("BESZEL_AGENT_TOKEN")
os.Unsetenv("TOKEN")
os.Unsetenv("BESZEL_AGENT_TOKEN_FILE")
os.Unsetenv("TOKEN_FILE")
}
t.Run("token from TOKEN environment variable", func(t *testing.T) {
unsetEnvVars()
// Set TOKEN env var
expectedToken := "test-token-from-env"
os.Setenv("TOKEN", expectedToken)
defer os.Unsetenv("TOKEN")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("token from BESZEL_AGENT_TOKEN environment variable", func(t *testing.T) {
unsetEnvVars()
// Set BESZEL_AGENT_TOKEN env var (should take precedence)
expectedToken := "test-token-from-beszel-env"
os.Setenv("BESZEL_AGENT_TOKEN", expectedToken)
defer os.Unsetenv("BESZEL_AGENT_TOKEN")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("token from TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file
expectedToken := "test-token-from-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(expectedToken)
require.NoError(t, err)
tokenFile.Close()
// Set TOKEN_FILE env var
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("token from BESZEL_AGENT_TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file
expectedToken := "test-token-from-beszel-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(expectedToken)
require.NoError(t, err)
tokenFile.Close()
// Set BESZEL_AGENT_TOKEN_FILE env var (should take precedence)
os.Setenv("BESZEL_AGENT_TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("BESZEL_AGENT_TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("TOKEN takes precedence over TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file
fileToken := "token-from-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(fileToken)
require.NoError(t, err)
tokenFile.Close()
// Set both TOKEN and TOKEN_FILE
envToken := "token-from-env"
os.Setenv("TOKEN", envToken)
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer func() {
os.Unsetenv("TOKEN")
os.Unsetenv("TOKEN_FILE")
}()
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, envToken, token, "TOKEN should take precedence over TOKEN_FILE")
})
t.Run("error when neither TOKEN nor TOKEN_FILE is set", func(t *testing.T) {
unsetEnvVars()
token, err := getToken()
assert.Error(t, err)
assert.Equal(t, "", token)
assert.Contains(t, err.Error(), "must set TOKEN or TOKEN_FILE")
})
t.Run("error when TOKEN_FILE points to non-existent file", func(t *testing.T) {
unsetEnvVars()
// Set TOKEN_FILE to a non-existent file
os.Setenv("TOKEN_FILE", "/non/existent/file.txt")
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.Error(t, err)
assert.Equal(t, "", token)
assert.Contains(t, err.Error(), "no such file or directory")
})
t.Run("handles empty token file", func(t *testing.T) {
unsetEnvVars()
// Create an empty token file
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
tokenFile.Close()
// Set TOKEN_FILE env var
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, "", token, "Empty file should return empty string")
})
}

View File

@@ -9,35 +9,30 @@ import (
)
// getDataDir returns the path to the data directory for the agent and an error
// if the directory is not valid. Pass an empty string to attempt to find the
// optimal data directory.
func getDataDir(dataDir string) (string, error) {
if dataDir == "" {
dataDir, _ = GetEnv("DATA_DIR")
// 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) {
if len(dataDirs) > 0 {
return testDataDirs(dataDirs)
}
dataDir, _ := GetEnv("DATA_DIR")
if dataDir != "" {
return testDataDirs([]string{dataDir})
dataDirs = append(dataDirs, dataDir)
}
var dirsToTry []string
if runtime.GOOS == "windows" {
dirsToTry = []string{
dataDirs = append(dataDirs,
filepath.Join(os.Getenv("APPDATA"), "beszel-agent"),
filepath.Join(os.Getenv("LOCALAPPDATA"), "beszel-agent"),
}
)
} else {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
dirsToTry = []string{
"/var/lib/beszel-agent",
filepath.Join(homeDir, ".config", "beszel"),
dataDirs = append(dataDirs, "/var/lib/beszel-agent")
if homeDir, err := os.UserHomeDir(); err == nil {
dataDirs = append(dataDirs, filepath.Join(homeDir, ".config", "beszel"))
}
}
return testDataDirs(dirsToTry)
return testDataDirs(dataDirs)
}
func testDataDirs(paths []string) (string, error) {

View File

@@ -44,15 +44,15 @@ func TestGetDataDir(t *testing.T) {
oldValue := os.Getenv("DATA_DIR")
defer func() {
if oldValue == "" {
os.Unsetenv("DATA_DIR")
os.Unsetenv("BESZEL_AGENT_DATA_DIR")
} else {
os.Setenv("DATA_DIR", oldValue)
os.Setenv("BESZEL_AGENT_DATA_DIR", oldValue)
}
}()
os.Setenv("DATA_DIR", tempDir)
os.Setenv("BESZEL_AGENT_DATA_DIR", tempDir)
result, err := getDataDir("")
result, err := getDataDir()
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
@@ -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 {

View File

@@ -0,0 +1,80 @@
using System;
using System.Globalization;
using LibreHardwareMonitor.Hardware;
class Program
{
static void Main()
{
var computer = new Computer
{
IsCpuEnabled = true,
IsGpuEnabled = true,
IsMemoryEnabled = true,
IsMotherboardEnabled = true,
IsStorageEnabled = true,
// IsPsuEnabled = true,
// IsNetworkEnabled = true,
};
computer.Open();
var reader = Console.In;
var writer = Console.Out;
string line;
while ((line = reader.ReadLine()) != null)
{
if (line.Trim().Equals("getTemps", StringComparison.OrdinalIgnoreCase))
{
foreach (var hw in computer.Hardware)
{
// process main hardware sensors
ProcessSensors(hw, writer);
// process subhardware sensors
foreach (var subhardware in hw.SubHardware)
{
ProcessSensors(subhardware, writer);
}
}
// send empty line to signal end of sensor data
writer.WriteLine();
writer.Flush();
}
}
computer.Close();
}
static void ProcessSensors(IHardware hardware, System.IO.TextWriter writer)
{
var updated = false;
foreach (var sensor in hardware.Sensors)
{
var validTemp = sensor.SensorType == SensorType.Temperature && sensor.Value.HasValue;
if (!validTemp || sensor.Name.Contains("Distance"))
{
continue;
}
if (!updated)
{
hardware.Update();
updated = true;
}
var name = sensor.Name;
// if sensor.Name starts with "Temperature" replace with hardware.Identifier but retain the rest of the name.
// usually this is a number like Temperature 3
if (sensor.Name.StartsWith("Temperature"))
{
name = hardware.Identifier.ToString().Replace("/", "_").TrimStart('_') + sensor.Name.Substring(11);
}
// invariant culture assures the value is parsable as a float
var value = sensor.Value.Value.ToString("0.##", CultureInfo.InvariantCulture);
// write the name and value to the writer
writer.WriteLine($"{name}|{value}");
}
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.4" />
</ItemGroup>
</Project>

View File

@@ -84,10 +84,10 @@ func (a *Agent) updateTemperatures(systemStats *system.Stats) {
// reset high temp
a.systemInfo.DashboardTemp = 0
temps, err := a.getTempsWithPanicRecovery(sensors.TemperaturesWithContext)
temps, err := a.getTempsWithPanicRecovery(getSensorTemps)
if err != nil {
// retry once on panic (gopsutil/issues/1832)
temps, err = a.getTempsWithPanicRecovery(sensors.TemperaturesWithContext)
temps, err = a.getTempsWithPanicRecovery(getSensorTemps)
if err != nil {
slog.Warn("Error updating temperatures", "err", err)
if len(systemStats.Temperatures) > 0 {

View File

@@ -0,0 +1,9 @@
//go:build !windows
package agent
import (
"github.com/shirou/gopsutil/v4/sensors"
)
var getSensorTemps = sensors.TemperaturesWithContext

View File

@@ -0,0 +1,281 @@
//go:build windows
//go:generate dotnet build -c Release lhm/beszel_lhm.csproj
package agent
import (
"bufio"
"context"
"embed"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/shirou/gopsutil/v4/sensors"
)
// Note: This is always called from Agent.gatherStats() which holds Agent.Lock(),
// so no internal concurrency protection is needed.
// lhmProcess is a wrapper around the LHM .NET process.
type lhmProcess struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
scanner *bufio.Scanner
isRunning bool
stoppedNoSensors bool
consecutiveNoSensors uint8
execPath string
tempDir string
}
//go:embed all:lhm/bin/Release/net48
var lhmFs embed.FS
var (
beszelLhm *lhmProcess
beszelLhmOnce sync.Once
)
var errNoSensors = errors.New("no sensors found (try running as admin)")
// newlhmProcess copies the embedded LHM executable to a temporary directory and starts it.
func newlhmProcess() (*lhmProcess, error) {
destDir := filepath.Join(os.TempDir(), "beszel")
execPath := filepath.Join(destDir, "beszel_lhm.exe")
if err := os.MkdirAll(destDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
// Only copy if executable doesn't exist
if _, err := os.Stat(execPath); os.IsNotExist(err) {
if err := copyEmbeddedDir(lhmFs, "lhm/bin/Release/net48", destDir); err != nil {
return nil, fmt.Errorf("failed to copy embedded directory: %w", err)
}
}
lhm := &lhmProcess{
execPath: execPath,
tempDir: destDir,
}
if err := lhm.startProcess(); err != nil {
return nil, fmt.Errorf("failed to start process: %w", err)
}
return lhm, nil
}
// startProcess starts the external LHM process
func (lhm *lhmProcess) startProcess() error {
// Clean up any existing process
lhm.cleanupProcess()
cmd := exec.Command(lhm.execPath)
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
stdin.Close()
return err
}
if err := cmd.Start(); err != nil {
stdin.Close()
stdout.Close()
return err
}
// Update process state
lhm.cmd = cmd
lhm.stdin = stdin
lhm.stdout = stdout
lhm.scanner = bufio.NewScanner(stdout)
lhm.isRunning = true
// Give process a moment to initialize
time.Sleep(100 * time.Millisecond)
return nil
}
// cleanupProcess terminates the process and closes resources but preserves files
func (lhm *lhmProcess) cleanupProcess() {
lhm.isRunning = false
if lhm.cmd != nil && lhm.cmd.Process != nil {
lhm.cmd.Process.Kill()
lhm.cmd.Wait()
}
if lhm.stdin != nil {
lhm.stdin.Close()
lhm.stdin = nil
}
if lhm.stdout != nil {
lhm.stdout.Close()
lhm.stdout = nil
}
lhm.cmd = nil
lhm.scanner = nil
lhm.stoppedNoSensors = false
lhm.consecutiveNoSensors = 0
}
func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {
if lhm.stoppedNoSensors {
// Fall back to gopsutil if we can't get sensors from LHM
return sensors.TemperaturesWithContext(ctx)
}
// Start process if it's not running
if !lhm.isRunning || lhm.stdin == nil || lhm.scanner == nil {
err := lhm.startProcess()
if err != nil {
return temps, err
}
}
// Send command to process
_, err = fmt.Fprintln(lhm.stdin, "getTemps")
if err != nil {
lhm.isRunning = false
return temps, fmt.Errorf("failed to send command: %w", err)
}
// Read all sensor lines until we hit an empty line or EOF
for lhm.scanner.Scan() {
line := strings.TrimSpace(lhm.scanner.Text())
if line == "" {
break
}
parts := strings.Split(line, "|")
if len(parts) != 2 {
slog.Debug("Invalid sensor format", "line", line)
continue
}
name := strings.TrimSpace(parts[0])
valueStr := strings.TrimSpace(parts[1])
value, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
slog.Debug("Failed to parse sensor", "err", err, "line", line)
continue
}
if name == "" || value <= 0 || value > 150 {
slog.Debug("Invalid sensor", "name", name, "val", value, "line", line)
continue
}
temps = append(temps, sensors.TemperatureStat{
SensorKey: name,
Temperature: value,
})
}
if err := lhm.scanner.Err(); err != nil {
lhm.isRunning = false
return temps, err
}
// Handle no sensors case
if len(temps) == 0 {
lhm.consecutiveNoSensors++
if lhm.consecutiveNoSensors >= 3 {
lhm.stoppedNoSensors = true
slog.Warn(errNoSensors.Error())
lhm.cleanup()
}
return sensors.TemperaturesWithContext(ctx)
}
lhm.consecutiveNoSensors = 0
return temps, nil
}
// getSensorTemps attempts to pull sensor temperatures from the embedded LHM process.
// NB: LibreHardwareMonitorLib requires admin privileges to access all available sensors.
func getSensorTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {
defer func() {
if err != nil {
slog.Debug("Error reading sensors", "err", err)
}
}()
// Initialize process once
beszelLhmOnce.Do(func() {
beszelLhm, err = newlhmProcess()
})
if err != nil {
return temps, fmt.Errorf("failed to initialize lhm: %w", err)
}
if beszelLhm == nil {
return temps, fmt.Errorf("lhm not available")
}
return beszelLhm.getTemps(ctx)
}
// cleanup terminates the process and closes resources
func (lhm *lhmProcess) cleanup() {
lhm.cleanupProcess()
if lhm.tempDir != "" {
os.RemoveAll(lhm.tempDir)
}
}
// copyEmbeddedDir copies the embedded directory to the destination path
func copyEmbeddedDir(fs embed.FS, srcPath, destPath string) error {
entries, err := fs.ReadDir(srcPath)
if err != nil {
return err
}
if err := os.MkdirAll(destPath, 0755); err != nil {
return err
}
for _, entry := range entries {
srcEntryPath := path.Join(srcPath, entry.Name())
destEntryPath := filepath.Join(destPath, entry.Name())
if entry.IsDir() {
if err := copyEmbeddedDir(fs, srcEntryPath, destEntryPath); err != nil {
return err
}
continue
}
data, err := fs.ReadFile(srcEntryPath)
if err != nil {
return err
}
if err := os.WriteFile(destEntryPath, data, 0755); err != nil {
return err
}
}
return nil
}

View File

@@ -80,10 +80,11 @@ func (a *Agent) getSystemStats() system.Stats {
// load average
if avgstat, err := load.Avg(); err == nil {
systemStats.LoadAvg1 = twoDecimals(avgstat.Load1)
systemStats.LoadAvg5 = twoDecimals(avgstat.Load5)
systemStats.LoadAvg15 = twoDecimals(avgstat.Load15)
slog.Debug("Load average", "5m", systemStats.LoadAvg5, "15m", systemStats.LoadAvg15)
// TODO: remove these in future release in favor of load avg array
systemStats.LoadAvg[0] = avgstat.Load1
systemStats.LoadAvg[1] = avgstat.Load5
systemStats.LoadAvg[2] = avgstat.Load15
slog.Debug("Load average", "5m", avgstat.Load5, "15m", avgstat.Load15)
} else {
slog.Error("Error getting load average", "err", err)
}
@@ -255,9 +256,11 @@ func (a *Agent) getSystemStats() system.Stats {
// update base system info
a.systemInfo.Cpu = systemStats.Cpu
a.systemInfo.LoadAvg1 = systemStats.LoadAvg1
a.systemInfo.LoadAvg5 = systemStats.LoadAvg5
a.systemInfo.LoadAvg15 = systemStats.LoadAvg15
a.systemInfo.LoadAvg = systemStats.LoadAvg
// TODO: remove these in future release in favor of load avg array
a.systemInfo.LoadAvg1 = systemStats.LoadAvg[0]
a.systemInfo.LoadAvg5 = systemStats.LoadAvg[1]
a.systemInfo.LoadAvg15 = systemStats.LoadAvg[2]
a.systemInfo.MemPct = systemStats.MemPct
a.systemInfo.DiskPct = systemStats.DiskPct
a.systemInfo.Uptime, _ = host.Uptime()

View File

@@ -47,9 +47,7 @@ type SystemAlertStats struct {
NetSent float64 `json:"ns"`
NetRecv float64 `json:"nr"`
Temperatures map[string]float32 `json:"t"`
LoadAvg1 float64 `json:"l1"`
LoadAvg5 float64 `json:"l5"`
LoadAvg15 float64 `json:"l15"`
LoadAvg [3]float64 `json:"la"`
}
type SystemAlertData struct {

View File

@@ -55,13 +55,13 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
val = data.Info.DashboardTemp
unit = "°C"
case "LoadAvg1":
val = data.Info.LoadAvg1
val = data.Info.LoadAvg[0]
unit = ""
case "LoadAvg5":
val = data.Info.LoadAvg5
val = data.Info.LoadAvg[1]
unit = ""
case "LoadAvg15":
val = data.Info.LoadAvg15
val = data.Info.LoadAvg[2]
unit = ""
}
@@ -200,11 +200,11 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
alert.mapSums[key] += temp
}
case "LoadAvg1":
alert.val += stats.LoadAvg1
alert.val += stats.LoadAvg[0]
case "LoadAvg5":
alert.val += stats.LoadAvg5
alert.val += stats.LoadAvg[1]
case "LoadAvg15":
alert.val += stats.LoadAvg15
alert.val += stats.LoadAvg[2]
default:
continue
}
@@ -293,18 +293,11 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
// app.Logger().Error("failed to save alert record", "err", err)
return
}
// expand the user relation and send the alert
if errs := am.hub.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
return
}
if user := alert.alertRecord.ExpandedOne("user"); user != nil {
am.SendAlert(AlertMessageData{
UserID: user.Id,
Title: subject,
Message: body,
Link: am.hub.MakeLink("system", systemName),
LinkText: "View " + systemName,
})
}
am.SendAlert(AlertMessageData{
UserID: alert.alertRecord.GetString("user"),
Title: subject,
Message: body,
Link: am.hub.MakeLink("system", systemName),
LinkText: "View " + systemName,
})
}

View File

@@ -31,11 +31,13 @@ type Stats struct {
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty,omitzero"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty,omitzero"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty,omitzero"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
// TODO: remove other load fields in future release in favor of load avg array
}
type GPUData struct {
@@ -79,25 +81,27 @@ const (
)
type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Cores int `json:"c" cbor:"2,keyasint"`
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
CpuModel string `json:"m" cbor:"4,keyasint"`
Uptime uint64 `json:"u" cbor:"5,keyasint"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
MemPct float64 `json:"mp" cbor:"7,keyasint"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
AgentVersion string `json:"v" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Cores int `json:"c" cbor:"2,keyasint"`
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
CpuModel string `json:"m" cbor:"4,keyasint"`
Uptime uint64 `json:"u" cbor:"5,keyasint"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
MemPct float64 `json:"mp" cbor:"7,keyasint"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
AgentVersion string `json:"v" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
// TODO: remove load fields in future release in favor of load avg array
}
// Final data structure to return to the hub

View File

@@ -203,9 +203,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.DiskWritePs += stats.DiskWritePs
sum.NetworkSent += stats.NetworkSent
sum.NetworkRecv += stats.NetworkRecv
sum.LoadAvg1 += stats.LoadAvg1
sum.LoadAvg5 += stats.LoadAvg5
sum.LoadAvg15 += stats.LoadAvg15
sum.LoadAvg[0] += stats.LoadAvg[0]
sum.LoadAvg[1] += stats.LoadAvg[1]
sum.LoadAvg[2] += stats.LoadAvg[2]
sum.Bandwidth[0] += stats.Bandwidth[0]
sum.Bandwidth[1] += stats.Bandwidth[1]
// Set peak values
@@ -285,9 +285,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count)
sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count)
sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count)
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
sum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count)
sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
// Average temperatures

View File

@@ -6,6 +6,7 @@ import (
"log"
"net/http"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
@@ -13,18 +14,6 @@ type UserManager struct {
app core.App
}
type UserSettings struct {
ChartTime string `json:"chartTime"`
NotificationEmails []string `json:"emails"`
NotificationWebhooks []string `json:"webhooks"`
// UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
// UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits
// UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits
// New field for alert history retention (e.g., "1m", "3m", "6m", "1y")
AlertHistoryRetention string `json:"alertHistoryRetention,omitempty"`
}
func NewUserManager(app core.App) *UserManager {
return &UserManager{
app: app,
@@ -42,29 +31,26 @@ func (um *UserManager) InitializeUserRole(e *core.RecordEvent) error {
// Initialize user settings with defaults if not set
func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
record := e.Record
// intialize settings with defaults
settings := UserSettings{
ChartTime: "1h",
NotificationEmails: []string{},
NotificationWebhooks: []string{},
// intialize settings with defaults (zero values can be ignored)
settings := struct {
ChartTime string `json:"chartTime"`
Emails []string `json:"emails"`
}{
ChartTime: "1h",
}
record.UnmarshalJSONField("settings", &settings)
if len(settings.NotificationEmails) == 0 {
// get user email from auth record
if errs := um.app.ExpandRecord(record, []string{"user"}, nil); len(errs) == 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
if user := record.ExpandedOne("user"); user != nil {
settings.NotificationEmails = []string{user.GetString("email")}
} else {
log.Println("Failed to get user email from auth record")
}
} else {
log.Println("failed to expand user relation", "errs", errs)
}
// get user email from auth record
var user struct {
Email string `db:"email"`
}
// if len(settings.NotificationWebhooks) == 0 {
// settings.NotificationWebhooks = []string{""}
// }
err := e.App.DB().NewQuery("SELECT email FROM users WHERE id = {:id}").Bind(dbx.Params{
"id": record.GetString("user"),
}).One(&user)
if err != nil {
log.Println("failed to get user email", "err", err)
return err
}
settings.Emails = []string{user.Email}
record.Set("settings", settings)
return e.Next()
}

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.12.0",
"version": "0.12.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.12.0",
"version": "0.12.2",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.12.0",
"version": "0.12.2",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -72,18 +72,18 @@ function AlertDialogContent({ system }: { system: SystemRecord }) {
const alerts = useStore($alerts)
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
// alertsSignature changes only when alerts for this system change
let alertsSignature = ""
/* key to prevent re-rendering */
const alertsSignature: string[] = []
const systemAlerts = alerts.filter((alert) => {
if (alert.system === system.id) {
alertsSignature += alert.name + alert.min + alert.value
alertsSignature.push(alert.name, alert.min, alert.value)
return true
}
return false
}) as AlertRecord[]
return useMemo(() => {
// console.log("render modal", system.name, alertsSignature)
const data = Object.keys(alertInfo).map((name) => {
const alert = alertInfo[name as keyof typeof alertInfo]
return {
@@ -149,5 +149,5 @@ function AlertDialogContent({ system }: { system: SystemRecord }) {
</Tabs>
</>
)
}, [alertsSignature, overwriteExisting])
}, [alertsSignature.join(""), overwriteExisting])
}

View File

@@ -87,5 +87,5 @@ export default function AreaChartDefault({
</ChartContainer>
</div>
)
}, [chartData.systemStats.length, yAxisWidth, maxToggled])
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
}

View File

@@ -9,31 +9,30 @@ import {
xAxis,
} from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData } from "@/types"
import { ChartData, SystemStats } from "@/types"
import { memo } from "react"
import { t } from "@lingui/core/macro"
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
if (chartData.systemStats.length === 0) {
return null
}
const keys = {
l1: {
const keys: { legacy: keyof SystemStats; color: string; label: string }[] = [
{
legacy: "l1",
color: "hsl(271, 81%, 60%)", // Purple
label: t({ message: `1 min`, comment: "Load average" }),
},
l5: {
{
legacy: "l5",
color: "hsl(217, 91%, 60%)", // Blue
label: t({ message: `5 min`, comment: "Load average" }),
},
l15: {
{
legacy: "l15",
color: "hsl(25, 95%, 53%)", // Orange
label: t({ message: `15 min`, comment: "Load average" }),
},
}
]
return (
<div>
@@ -69,16 +68,22 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD
/>
}
/>
{Object.entries(keys).map(([key, value]: [string, { color: string; label: string }]) => {
{keys.map(({ legacy, color, label }, i) => {
const dataKey = (value: { stats: SystemStats }) => {
if (chartData.agentVersion.patch < 1) {
return value.stats?.[legacy]
}
return value.stats?.la?.[i] ?? value.stats?.[legacy]
}
return (
<Line
key={key}
dataKey={`stats.${key}`}
name={value.label}
key={i}
dataKey={dataKey}
name={label}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={value.color}
stroke={color}
isAnimationActive={false}
/>
)

View File

@@ -1,5 +1,5 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -43,6 +43,14 @@ const showLoginFaliedToast = () => {
})
}
const getAuthProviderIcon = (provider: AuthProviderInfo) => {
let { name } = provider
if (name.startsWith("oidc")) {
name = "oidc"
}
return prependBasePath(`/_/images/oauth2/${name}.svg`)
}
export function UserAuthForm({
className,
isFirstRun,
@@ -165,8 +173,8 @@ export function UserAuthForm({
}, [])
return (
<div className={cn("grid gap-6", className)} {...props}>
{passwordEnabled && (
<div className={cn("grid gap-6", className)} {...props}>
{passwordEnabled && (
<>
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
<div className="grid gap-2.5">
@@ -242,20 +250,20 @@ export function UserAuthForm({
</form>
{(isFirstRun || oauthEnabled) && (
// only show 'continue with' during onboarding or if we have auth providers
(<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
<Trans>Or continue with</Trans>
</span>
</div>
</div>)
</div>
)}
</>
)}
{oauthEnabled && (
{oauthEnabled && (
<div className="grid gap-2 -mt-1">
{authMethods.oauth2.providers.map((provider) => (
<button
@@ -273,7 +281,7 @@ export function UserAuthForm({
) : (
<img
className="me-2 h-4 w-4 dark:brightness-0 dark:invert"
src={prependBasePath(`/_/images/oauth2/${provider.name}.svg`)}
src={getAuthProviderIcon(provider)}
alt=""
// onError={(e) => {
// e.currentTarget.src = "/static/lock.svg"
@@ -285,16 +293,16 @@ export function UserAuthForm({
))}
</div>
)}
{!oauthEnabled && isFirstRun && (
{!oauthEnabled && isFirstRun && (
// only show GitHub button / dialog during onboarding
(<Dialog>
<DialogTrigger asChild>
<Dialog>
<DialogTrigger asChild>
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
<img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" />
<span className="translate-y-[1px]">GitHub</span>
</button>
</DialogTrigger>
<DialogContent style={{ maxWidth: 440, width: "90%" }}>
<DialogContent style={{ maxWidth: 440, width: "90%" }}>
<DialogHeader>
<DialogTitle>
<Trans>OAuth 2 / OIDC support</Trans>
@@ -318,9 +326,9 @@ export function UserAuthForm({
</p>
</div>
</DialogContent>
</Dialog>)
</Dialog>
)}
{passwordEnabled && !isFirstRun && (
{passwordEnabled && !isFirstRun && (
<Link
href={getPagePath($router, "forgot_password")}
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
@@ -328,6 +336,6 @@ export function UserAuthForm({
<Trans>Forgot password?</Trans>
</Link>
)}
</div>
);
</div>
)
}

View File

@@ -18,7 +18,9 @@ export const Home = memo(() => {
const systems = useStore($systems)
const { t } = useLingui()
let alertsKey = ""
/* key to prevent re-rendering of active alerts */
const alertsKey: string[] = []
const activeAlerts = useMemo(() => {
const activeAlerts = alerts.filter((alert) => {
const active = alert.triggered && alert.name in alertInfo
@@ -26,7 +28,7 @@ export const Home = memo(() => {
return false
}
alert.sysname = systems.find((system) => system.id === alert.system)?.name
alertsKey += alert.id
alertsKey.push(alert.id)
return true
})
return activeAlerts
@@ -81,7 +83,7 @@ export const Home = memo(() => {
</div>
</>
),
[alertsKey]
[alertsKey.join("")]
)
})

View File

@@ -74,11 +74,11 @@ export default function AlertsHistoryDataTable() {
}
// Initial load
pb.collection<AlertsHistoryRecord>("alerts_history")
.getFullList({
.getList(0, 200, {
...pbOptions,
sort: "-created",
})
.then((records) => setData(records))
.then(({ items }) => setData(items))
// Subscribe to changes
;(async () => {

View File

@@ -11,6 +11,7 @@ import { useState } from "react"
import languages from "@/lib/languages"
import { dynamicActivate } from "@/lib/i18n"
import { useLingui } from "@lingui/react/macro"
import { Input } from "@/components/ui/input"
import { Unit } from "@/lib/enums"
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
@@ -133,7 +134,6 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="block" htmlFor="unitNet">
<Trans comment="Context: Bytes or bits">Network unit</Trans>
@@ -156,7 +156,6 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="block" htmlFor="unitDisk">
<Trans>Disk unit</Trans>
@@ -182,6 +181,47 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</div>
</div>
<Separator />
<div className="space-y-2">
<div className="mb-4">
<h3 className="mb-1 text-lg font-medium">
<Trans>Warning thresholds</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Set percentage thresholds for meter colors.</Trans>
</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 items-end">
<div className="space-y-1">
<Label htmlFor="colorWarn">
<Trans>Warning (%)</Trans>
</Label>
<Input
id="colorWarn"
name="colorWarn"
type="number"
min={1}
max={100}
className="min-w-24"
defaultValue={userSettings.colorWarn ?? 65}
/>
</div>
<div className="space-y-1">
<Label htmlFor="colorCrit">
<Trans>Critical (%)</Trans>
</Label>
<Input
id="colorCrit"
name="colorCrit"
type="number"
min={1}
max={100}
className="min-w-24"
defaultValue={userSettings.colorCrit ?? 90}
/>
</div>
</div>
</div>
<Separator />
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
<Trans>Save Settings</Trans>

View File

@@ -10,7 +10,7 @@ import { getPagePath, redirectPage } from "@nanostores/router"
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
import { $userSettings, pb } from "@/lib/stores.ts"
import { toast } from "@/components/ui/use-toast.ts"
import { UserSettings } from "@/types.js"
import { UserSettings } from "@/types"
import General from "./general.tsx"
import Notifications from "./notifications.tsx"
import ConfigYaml from "./config-yaml.tsx"

View File

@@ -42,6 +42,10 @@ const pbFingerprintOptions = {
fields: "id,fingerprint,token,system,expand.system.name",
}
function sortFingerprints(fingerprints: FingerprintRecord[]) {
return fingerprints.sort((a, b) => a.expand.system.name.localeCompare(b.expand.system.name))
}
const SettingsFingerprintsPage = memo(() => {
if (isReadOnlyUser()) {
redirectPage($router, "settings", { name: "general" })
@@ -51,9 +55,10 @@ const SettingsFingerprintsPage = memo(() => {
// Get fingerprint records on mount
useEffect(() => {
pb.collection("fingerprints")
.getFullList(pbFingerprintOptions)
// @ts-ignore
.then(setFingerprints)
.getFullList<FingerprintRecord>(pbFingerprintOptions)
.then((prints) => {
setFingerprints(sortFingerprints(prints))
})
}, [])
// Subscribe to fingerprint updates
@@ -66,7 +71,7 @@ const SettingsFingerprintsPage = memo(() => {
(res) => {
setFingerprints((currentFingerprints) => {
if (res.action === "create") {
return [...currentFingerprints, res.record as FingerprintRecord]
return sortFingerprints([...currentFingerprints, res.record as FingerprintRecord])
}
if (res.action === "update") {
return currentFingerprints.map((fingerprint) => {

View File

@@ -26,6 +26,7 @@ import {
getHostDisplayValue,
getPbTimestamp,
listen,
parseSemVer,
toFixedFloat,
useLocalStorage,
} from "@/lib/utils"
@@ -191,6 +192,7 @@ export default function SystemDetail({ name }: { name: string }) {
chartTime,
orientation: direction === "rtl" ? "right" : "left",
...getTimeData(chartTime, lastCreated),
agentVersion: parseSemVer(system?.info?.v),
}
}, [systemStats, containerData, direction])
@@ -642,7 +644,7 @@ export default function SystemDetail({ name }: { name: string }) {
)}
{/* Load Average chart */}
{system.info.l1 !== undefined && (
{chartData.agentVersion?.minor >= 12 && (
<ChartCard
empty={dataEmpty}
grid={grid}

View File

@@ -0,0 +1,425 @@
import { SystemRecord } from "@/types"
import { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table"
import { ClassValue } from "clsx"
import {
ArrowUpDownIcon,
CopyIcon,
CpuIcon,
HardDriveIcon,
MemoryStickIcon,
MoreHorizontalIcon,
PauseCircleIcon,
PenBoxIcon,
PlayCircleIcon,
ServerIcon,
Trash2Icon,
WifiIcon,
} from "lucide-react"
import { Button } from "../ui/button"
import {
cn,
copyToClipboard,
decimalString,
formatBytes,
formatTemperature,
getMeterState,
isReadOnlyUser,
parseSemVer,
} from "@/lib/utils"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useStore } from "@nanostores/react"
import { $userSettings, pb } from "@/lib/stores"
import { Trans, useLingui } from "@lingui/react/macro"
import { useMemo, useRef, useState } from "react"
import { memo } from "react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu"
import AlertButton from "../alerts/alert-button"
import { Dialog } from "../ui/dialog"
import { SystemDialog } from "../add-system"
import { AlertDialog } from "../ui/alert-dialog"
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog"
import { buttonVariants } from "../ui/button"
import { t } from "@lingui/core/macro"
import { MeterState } from "@/lib/enums"
/**
* @param viewMode - "table" or "grid"
* @returns - Column definitions for the systems table
*/
export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
const statusTranslations = {
up: () => t`Up`.toLowerCase(),
down: () => t`Down`.toLowerCase(),
paused: () => t`Paused`.toLowerCase(),
}
return [
{
size: 200,
minSize: 0,
accessorKey: "name",
id: "system",
name: () => t`System`,
filterFn: (row, _, filterVal) => {
const filterLower = filterVal.toLowerCase()
const { name, status } = row.original
// Check if the filter matches the name or status for this row
if (
name.toLowerCase().includes(filterLower) ||
statusTranslations[status as keyof typeof statusTranslations]?.().includes(filterLower)
) {
return true
}
return false
},
enableHiding: false,
invertSorting: false,
Icon: ServerIcon,
cell: (info) => (
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1 md:pe-5">
<IndicatorDot system={info.row.original} />
{info.getValue() as string}
</span>
),
header: sortableHeader,
},
{
accessorFn: ({ info }) => info.cpu,
id: "cpu",
name: () => t`CPU`,
cell: TableCellWithMeter,
Icon: CpuIcon,
header: sortableHeader,
},
{
// accessorKey: "info.mp",
accessorFn: ({ info }) => info.mp,
id: "memory",
name: () => t`Memory`,
cell: TableCellWithMeter,
Icon: MemoryStickIcon,
header: sortableHeader,
},
{
accessorFn: ({ info }) => info.dp,
id: "disk",
name: () => t`Disk`,
cell: TableCellWithMeter,
Icon: HardDriveIcon,
header: sortableHeader,
},
{
accessorFn: ({ info }) => info.g,
id: "gpu",
name: () => "GPU",
cell: TableCellWithMeter,
Icon: GpuIcon,
header: sortableHeader,
},
{
id: "loadAverage",
accessorFn: ({ info }) => {
const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
// TODO: remove this in future release in favor of la array
if (!sum) {
return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0)
}
return sum
},
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
size: 0,
Icon: HourglassIcon,
header: sortableHeader,
cell(info: CellContext<SystemRecord, unknown>) {
const { info: sysInfo, status } = info.row.original
// agent version
const { minor, patch } = parseSemVer(sysInfo.v)
let loadAverages = sysInfo.la
// use legacy load averages if agent version is less than 12.1.0
if (!loadAverages || (minor === 12 && patch < 1)) {
loadAverages = [sysInfo.l1 ?? 0, sysInfo.l5 ?? 0, sysInfo.l15 ?? 0]
}
const max = Math.max(...loadAverages)
if (max === 0 && (status === "paused" || minor < 12)) {
return null
}
const normalizedLoad = max / (sysInfo.t ?? 1)
const threshold = getMeterState(normalizedLoad * 100)
return (
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
<span
className={cn("inline-block size-2 rounded-full me-0.5", {
"bg-green-500": threshold === MeterState.Good,
"bg-yellow-500": threshold === MeterState.Warn,
"bg-red-600": threshold === MeterState.Crit,
})}
/>
{loadAverages?.map((la, i) => (
<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>
))}
</div>
)
},
},
{
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024,
id: "net",
name: () => t`Net`,
size: 0,
Icon: EthernetIcon,
header: sortableHeader,
cell(info) {
const sys = info.row.original
if (sys.status === "paused") {
return null
}
const userSettings = useStore($userSettings)
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
return (
<span className="tabular-nums whitespace-nowrap">
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
</span>
)
},
},
{
accessorFn: ({ info }) => info.dt,
id: "temp",
name: () => t({ message: "Temp", comment: "Temperature label in systems table" }),
size: 50,
hideSort: true,
Icon: ThermometerIcon,
header: sortableHeader,
cell(info) {
const val = info.getValue() as number
if (!val) {
return null
}
const userSettings = useStore($userSettings)
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
return (
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
</span>
)
},
},
{
accessorFn: ({ info }) => info.v,
id: "agent",
name: () => t`Agent`,
// invertSorting: true,
size: 50,
Icon: WifiIcon,
hideSort: true,
header: sortableHeader,
cell(info) {
const version = info.getValue() as string
if (!version) {
return null
}
const system = info.row.original
return (
<span className={cn("flex gap-2 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
<IndicatorDot
system={system}
className={
(system.status !== "up" && "bg-primary/30") ||
(version === globalThis.BESZEL.HUB_VERSION && "bg-green-500") ||
"bg-yellow-500"
}
/>
<span className="truncate max-w-14">{info.getValue() as string}</span>
</span>
)
},
},
{
id: "actions",
// @ts-ignore
name: () => t({ message: "Actions", comment: "Table column" }),
size: 50,
cell: ({ row }) => (
<div className="flex justify-end items-center gap-1 -ms-3">
<AlertButton system={row.original} />
<ActionsButton system={row.original} />
</div>
),
},
] as ColumnDef<SystemRecord>[]
}
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
const { column } = context
// @ts-ignore
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
return (
<Button
variant="ghost"
className="h-9 px-3 flex"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{Icon && <Icon className="me-2 size-4" />}
{name()}
{hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
</Button>
)
}
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const val = Number(info.getValue()) || 0
const threshold = getMeterState(val)
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span
className={cn(
"absolute inset-0 w-full h-full origin-left",
(info.row.original.status !== "up" && "bg-primary/30") ||
(threshold === MeterState.Good && "bg-green-500") ||
(threshold === MeterState.Warn && "bg-yellow-500") ||
"bg-red-600"
)}
style={{
transform: `scalex(${val / 100})`,
}}
></span>
</span>
</div>
)
}
export function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) {
className ||= {
"bg-green-500": system.status === "up",
"bg-red-500": system.status === "down",
"bg-primary/40": system.status === "paused",
"bg-yellow-500": system.status === "pending",
}
return (
<span
className={cn("flex-shrink-0 size-2 rounded-full", className)}
// style={{ marginBottom: "-1px" }}
/>
)
}
export const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
const [deleteOpen, setDeleteOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
let editOpened = useRef(false)
const { t } = useLingui()
const { id, status, host, name } = system
return useMemo(() => {
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"} data-nolink>
<span className="sr-only">
<Trans>Open menu</Trans>
</span>
<MoreHorizontalIcon className="w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!isReadOnlyUser() && (
<DropdownMenuItem
onSelect={() => {
editOpened.current = true
setEditOpen(true)
}}
>
<PenBoxIcon className="me-2.5 size-4" />
<Trans>Edit</Trans>
</DropdownMenuItem>
)}
<DropdownMenuItem
className={cn(isReadOnlyUser() && "hidden")}
onClick={() => {
pb.collection("systems").update(id, {
status: status === "paused" ? "pending" : "paused",
})
}}
>
{status === "paused" ? (
<>
<PlayCircleIcon className="me-2.5 size-4" />
<Trans>Resume</Trans>
</>
) : (
<>
<PauseCircleIcon className="me-2.5 size-4" />
<Trans>Pause</Trans>
</>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyToClipboard(name)}>
<CopyIcon className="me-2.5 size-4" />
<Trans>Copy name</Trans>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
<CopyIcon className="me-2.5 size-4" />
<Trans>Copy host</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")} onSelect={() => setDeleteOpen(true)}>
<Trash2Icon className="me-2.5 size-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* edit dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}
</Dialog>
{/* deletion dialog */}
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans>Are you sure you want to delete {name}?</Trans>
</AlertDialogTitle>
<AlertDialogDescription>
<Trans>
This action cannot be undone. This will permanently delete all current records for {name} from the
database.
</Trans>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>Cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction
className={cn(buttonVariants({ variant: "destructive" }))}
onClick={() => pb.collection("systems").delete(id)}
>
<Trans>Continue</Trans>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}, [id, status, host, name, t, deleteOpen, editOpen])
})

View File

@@ -1,5 +1,4 @@
import {
CellContext,
ColumnDef,
ColumnFiltersState,
getFilteredRowModel,
@@ -9,14 +8,13 @@ import {
VisibilityState,
getCoreRowModel,
useReactTable,
HeaderContext,
Row,
Table as TableType,
} from "@tanstack/react-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button, buttonVariants } from "@/components/ui/button"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
@@ -29,104 +27,30 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { SystemRecord } from "@/types"
import {
MoreHorizontalIcon,
ArrowUpDownIcon,
MemoryStickIcon,
CopyIcon,
PauseCircleIcon,
PlayCircleIcon,
Trash2Icon,
WifiIcon,
HardDriveIcon,
ServerIcon,
CpuIcon,
LayoutGridIcon,
LayoutListIcon,
ArrowDownIcon,
ArrowUpIcon,
Settings2Icon,
EyeIcon,
PenBoxIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $systems, $userSettings, pb } from "@/lib/stores"
import { memo, useEffect, useMemo, useState } from "react"
import { $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import {
cn,
copyToClipboard,
isReadOnlyUser,
useLocalStorage,
formatTemperature,
decimalString,
formatBytes,
} from "@/lib/utils"
import AlertsButton from "../alerts/alert-button"
import { cn, useLocalStorage } from "@/lib/utils"
import { $router, Link, navigate } from "../router"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useLingui, Trans } from "@lingui/react/macro"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "../ui/input"
import { ClassValue } from "clsx"
import { getPagePath } from "@nanostores/router"
import { SystemDialog } from "../add-system"
import { Dialog } from "../ui/dialog"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
import AlertButton from "../alerts/alert-button"
type ViewMode = "table" | "grid"
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = (info.getValue() as number) || 0
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{decimalString(val, 1)}%</span>
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span
className={cn(
"absolute inset-0 w-full h-full origin-left",
(info.row.original.status !== "up" && "bg-primary/30") ||
(val < 65 && "bg-green-500") ||
(val < 90 && "bg-yellow-500") ||
"bg-red-600"
)}
style={{
transform: `scalex(${val / 100})`,
}}
></span>
</span>
</div>
)
}
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
const { column } = context
// @ts-ignore
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
return (
<Button
variant="ghost"
className="h-9 px-3 flex"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{Icon && <Icon className="me-2 size-4" />}
{name()}
{hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
</Button>
)
}
export default function SystemsTable() {
const data = useStore($systems)
const { i18n, t } = useLingui()
@@ -144,208 +68,7 @@ export default function SystemsTable() {
}
}, [filter])
const columnDefs = useMemo(() => {
const statusTranslations = {
up: () => t`Up`.toLowerCase(),
down: () => t`Down`.toLowerCase(),
paused: () => t`Paused`.toLowerCase(),
}
return [
{
size: 200,
minSize: 0,
accessorKey: "name",
id: "system",
name: () => t`System`,
filterFn: (row, _, filterVal) => {
const filterLower = filterVal.toLowerCase()
const { name, status } = row.original
// Check if the filter matches the name or status for this row
if (
name.toLowerCase().includes(filterLower) ||
statusTranslations[status as keyof typeof statusTranslations]?.().includes(filterLower)
) {
return true
}
return false
},
enableHiding: false,
invertSorting: false,
Icon: ServerIcon,
cell: (info) => (
<span className="flex gap-0.5 items-center text-base md:ps-1 md:pe-5">
<IndicatorDot system={info.row.original} />
<Button
data-nolink
variant={"ghost"}
className="text-primary/90 h-7 px-1.5 gap-1.5"
onClick={() => copyToClipboard(info.getValue() as string)}
>
{info.getValue() as string}
<CopyIcon className="size-2.5" />
</Button>
</span>
),
header: sortableHeader,
},
{
accessorFn: ({ info }) => decimalString(info.cpu, info.cpu >= 10 ? 1 : 2),
id: "cpu",
name: () => t`CPU`,
cell: CellFormatter,
Icon: CpuIcon,
header: sortableHeader,
},
{
// accessorKey: "info.mp",
accessorFn: (originalRow) => originalRow.info.mp,
id: "memory",
name: () => t`Memory`,
cell: CellFormatter,
Icon: MemoryStickIcon,
header: sortableHeader,
},
{
accessorFn: (originalRow) => originalRow.info.dp,
id: "disk",
name: () => t`Disk`,
cell: CellFormatter,
Icon: HardDriveIcon,
header: sortableHeader,
},
{
accessorFn: (originalRow) => originalRow.info.g,
id: "gpu",
name: () => "GPU",
cell: CellFormatter,
Icon: GpuIcon,
header: sortableHeader,
},
{
id: "loadAverage",
accessorFn: ({ info }) => {
const { l1 = 0, l5 = 0, l15 = 0 } = info
return l1 + l5 + l15
},
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
size: 0,
Icon: HourglassIcon,
header: sortableHeader,
cell(info: CellContext<SystemRecord, unknown>) {
const { info: sysInfo, status } = info.row.original
if (sysInfo.l1 === undefined) {
return null
}
const { l1 = 0, l5 = 0, l15 = 0, t: cpuThreads = 1 } = sysInfo
const loadAverages = [l1, l5, l15]
function getDotColor() {
const max = Math.max(...loadAverages)
const normalized = max / cpuThreads
if (status !== "up") return "bg-primary/30"
if (normalized < 0.7) return "bg-green-500"
if (normalized < 1) return "bg-yellow-500"
return "bg-red-600"
}
return (
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
<span className={cn("inline-block size-2 rounded-full me-0.5", getDotColor())} />
{loadAverages.map((la, i) => (
<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>
))}
</div>
)
},
},
{
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024,
id: "net",
name: () => t`Net`,
size: 0,
Icon: EthernetIcon,
header: sortableHeader,
cell(info) {
const sys = info.row.original
if (sys.status === "paused") {
return null
}
const userSettings = useStore($userSettings)
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
return (
<span className="tabular-nums whitespace-nowrap">
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
</span>
)
},
},
{
accessorFn: (originalRow) => originalRow.info.dt,
id: "temp",
name: () => t({ message: "Temp", comment: "Temperature label in systems table" }),
size: 50,
hideSort: true,
Icon: ThermometerIcon,
header: sortableHeader,
cell(info) {
const val = info.getValue() as number
if (!val) {
return null
}
const userSettings = useStore($userSettings)
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
return (
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
</span>
)
},
},
{
accessorFn: (originalRow) => originalRow.info.v,
id: "agent",
name: () => t`Agent`,
// invertSorting: true,
size: 50,
Icon: WifiIcon,
hideSort: true,
header: sortableHeader,
cell(info) {
const version = info.getValue() as string
if (!version) {
return null
}
const system = info.row.original
return (
<span className={cn("flex gap-2 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
<IndicatorDot
system={system}
className={
(system.status !== "up" && "bg-primary/30") ||
(version === globalThis.BESZEL.HUB_VERSION && "bg-green-500") ||
"bg-yellow-500"
}
/>
<span className="truncate max-w-14">{info.getValue() as string}</span>
</span>
)
},
},
{
id: "actions",
// @ts-ignore
name: () => t({ message: "Actions", comment: "Table column" }),
size: 50,
cell: ({ row }) => (
<div className="flex justify-end items-center gap-1 -ms-3">
<AlertsButton system={row.original} />
<ActionsButton system={row.original} />
</div>
),
},
] as ColumnDef<SystemRecord>[]
}, [])
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [])
const table = useReactTable({
data,
@@ -623,7 +346,7 @@ const SystemCard = memo(
</CardTitle>
{table.getColumn("actions")?.getIsVisible() && (
<div className="flex gap-1 flex-shrink-0 relative z-10">
<AlertsButton system={system} />
<AlertButton system={system} />
<ActionsButton system={system} />
</div>
)}
@@ -658,116 +381,3 @@ const SystemCard = memo(
}, [system, colLength, t])
}
)
const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
const [deleteOpen, setDeleteOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
let editOpened = useRef(false)
const { t } = useLingui()
const { id, status, host, name } = system
return useMemo(() => {
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"} data-nolink>
<span className="sr-only">
<Trans>Open menu</Trans>
</span>
<MoreHorizontalIcon className="w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!isReadOnlyUser() && (
<DropdownMenuItem
onSelect={() => {
editOpened.current = true
setEditOpen(true)
}}
>
<PenBoxIcon className="me-2.5 size-4" />
<Trans>Edit</Trans>
</DropdownMenuItem>
)}
<DropdownMenuItem
className={cn(isReadOnlyUser() && "hidden")}
onClick={() => {
pb.collection("systems").update(id, {
status: status === "paused" ? "pending" : "paused",
})
}}
>
{status === "paused" ? (
<>
<PlayCircleIcon className="me-2.5 size-4" />
<Trans>Resume</Trans>
</>
) : (
<>
<PauseCircleIcon className="me-2.5 size-4" />
<Trans>Pause</Trans>
</>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
<CopyIcon className="me-2.5 size-4" />
<Trans>Copy host</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")} onSelect={() => setDeleteOpen(true)}>
<Trash2Icon className="me-2.5 size-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* edit dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}
</Dialog>
{/* deletion dialog */}
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans>Are you sure you want to delete {name}?</Trans>
</AlertDialogTitle>
<AlertDialogDescription>
<Trans>
This action cannot be undone. This will permanently delete all current records for {name} from the
database.
</Trans>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>Cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction
className={cn(buttonVariants({ variant: "destructive" }))}
onClick={() => pb.collection("systems").delete(id)}
>
<Trans>Continue</Trans>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}, [id, status, host, name, t, deleteOpen, editOpen])
})
function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) {
className ||= {
"bg-green-500": system.status === "up",
"bg-red-500": system.status === "down",
"bg-primary/40": system.status === "paused",
"bg-yellow-500": system.status === "pending",
}
return (
<span
className={cn("flex-shrink-0 size-2 rounded-full", className)}
// style={{ marginBottom: "-1px" }}
/>
)
}

View File

@@ -21,3 +21,10 @@ export enum Unit {
Celsius,
Fahrenheit,
}
/** Meter state for color */
export enum MeterState {
Good,
Warn,
Crit,
}

View File

@@ -24,13 +24,21 @@ export const $chartTime = atom("1h") as PreinitializedWritableAtom<ChartTimes>
/** Whether to display average or max chart values */
export const $maxValues = atom(false)
// export const UserSettingsSchema = v.object({
// chartTime: v.picklist(["1h", "12h", "24h", "1w", "30d"]),
// emails: v.optional(v.array(v.pipe(v.string(), v.email())), [pb?.authStore?.record?.email ?? ""]),
// webhooks: v.optional(v.array(v.string())),
// colorWarn: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100))),
// colorDanger: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100))),
// unitTemp: v.optional(v.enum(Unit)),
// unitNet: v.optional(v.enum(Unit)),
// unitDisk: v.optional(v.enum(Unit)),
// })
/** User settings */
export const $userSettings = map<UserSettings>({
chartTime: "1h",
emails: [pb.authStore.record?.email || ""],
// unitTemp: "celsius",
// unitNet: "mbps",
// unitDisk: "mbps",
})
// update local storage on change
$userSettings.subscribe((value) => {

View File

@@ -9,6 +9,7 @@ import {
ChartTimeData,
ChartTimes,
FingerprintRecord,
SemVer,
SystemRecord,
UserSettings,
} from "@/types"
@@ -19,7 +20,7 @@ import { useEffect, useState } from "react"
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
import { prependBasePath } from "@/components/router"
import { Unit } from "./enums"
import { MeterState, Unit } from "./enums"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -495,3 +496,20 @@ export function formatDuration(
.filter(Boolean)
.join(" ")
}
export const parseSemVer = (semVer = ""): SemVer => {
// if (semVer.startsWith("v")) {
// semVer = semVer.slice(1)
// }
if (semVer.includes("-")) {
semVer = semVer.slice(0, semVer.indexOf("-"))
}
const parts = semVer.split(".").map(Number)
return { major: parts?.[0] ?? 0, minor: parts?.[1] ?? 0, patch: parts?.[2] ?? 0 }
}
/** Get meter state from 0-100 value. Used for color coding meters. */
export function getMeterState(value: number): MeterState {
const { colorWarn = 65, colorCrit = 90 } = $userSettings.get()
return value >= colorCrit ? MeterState.Crit : value >= colorWarn ? MeterState.Warn : MeterState.Good
}

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\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"
@@ -97,7 +97,7 @@ msgstr "إضافة نظام"
#: src/components/routes/settings/notifications.tsx
msgid "Add URL"
msgstr "إضافة عنوان URL"
msgstr "إضافة رابط"
#: src/components/routes/settings/general.tsx
msgid "Adjust display options for charts."
@@ -155,7 +155,7 @@ msgstr "المتوسط يتجاوز <0>{value}{0}</0>"
#: src/components/routes/system.tsx
msgid "Average power consumption of GPUs"
msgstr "متوسط ​​استهلاك طاقة GPUs"
msgstr "متوسط ​​استهلاك طاقة وحدة معالجة الرسوميات"
#: src/components/routes/system.tsx
msgid "Average system-wide CPU utilization"
@@ -178,7 +178,7 @@ msgstr "عرض النطاق الترددي"
#: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "يدعم Beszel OpenID Connect والعديد من مزودي المصادقة OAuth2."
msgstr "يدعم بيزيل بروتوكول OpenID Connect والعديد من مزوّدي المصادقة عبر بروتوكول OAuth2."
#: src/components/routes/settings/notifications.tsx
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
@@ -250,7 +250,7 @@ msgstr "تعليمات سطر الأوامر"
#: src/components/routes/settings/notifications.tsx
msgid "Configure how you receive alert notifications."
msgstr "قم بتكوين كيفية تلقي إشعارات التنبيه."
msgstr "هيئ التنبيهات الواردة"
#: src/components/login/auth-form.tsx
#: src/components/login/auth-form.tsx
@@ -274,13 +274,13 @@ msgstr "تم النسخ إلى الحافظة"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker compose file content"
msgid "Copy docker compose"
msgstr "نسخ docker compose"
msgstr "نسخ أمر تركيب الدوكر"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker run command"
msgid "Copy docker run"
msgstr "نسخ docker run"
msgstr "نسخ أمر تشغيل الدوكر"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
@@ -296,6 +296,10 @@ msgstr "نسخ المضيف"
msgid "Copy Linux command"
msgstr "نسخ أمر لينكس"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "نسخ الاسم"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "نسخ النص"
@@ -378,15 +382,15 @@ msgstr "استخدام القرص لـ {extraFsName}"
#: src/components/routes/system.tsx
msgid "Docker CPU Usage"
msgstr "استخدام المعالج لـ Docker"
msgstr "استخدام المعالج للدوكر"
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "استخدام الذاكرة لـ Docker"
msgstr "استخدام الذاكرة للدوكر"
#: src/components/routes/system.tsx
msgid "Docker Network I/O"
msgstr "إدخال/إخراج الشبكة لـ Docker"
msgstr "إدخال/إخراج الشبكة للدوكر"
#: src/components/command-palette.tsx
msgid "Documentation"
@@ -504,7 +508,7 @@ msgstr "عام"
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "استهلاك طاقة GPU"
msgstr "استهلاك طاقة وحدة معالجة الرسوميات"
#: src/components/systems-table/systems-table.tsx
msgid "Grid"
@@ -600,7 +604,7 @@ msgstr "تعليمات الإعداد اليدوي"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
msgid "Max 1 min"
msgstr "1 دقيقة كحد"
msgstr "الحد الأقصى دقيقة"
#: src/components/systems-table/systems-table.tsx
msgid "Memory"
@@ -613,7 +617,7 @@ msgstr "استخدام الذاكرة"
#: src/components/routes/system.tsx
msgid "Memory usage of docker containers"
msgstr "استخدام الذاكرة لحاويات Docker"
msgstr "استخدام الذاكرة لحاويات دوكر"
#: src/components/alerts-history-columns.tsx
#: src/components/add-system.tsx
@@ -626,7 +630,7 @@ msgstr "الشبكة"
#: src/components/routes/system.tsx
msgid "Network traffic of docker containers"
msgstr "حركة مرور الشبكة لحاويات Docker"
msgstr "حركة مرور الشبكة لحاويات الدوكر"
#: src/components/routes/system.tsx
msgid "Network traffic of public interfaces"
@@ -715,7 +719,7 @@ msgstr "إيقاف مؤقت"
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "متوقف مؤقتًا"
msgstr "متوقف مؤقتا"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -990,7 +994,7 @@ msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل
#: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
msgstr "يتم التفعيل عندما <EFBFBD><EFBFBD>تجاوز أي مستشعر عتبة معينة"
#: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold"
@@ -1092,7 +1096,7 @@ msgstr "عند التفعيل، يسمح هذا الرمز المميز للوك
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Windows command"
msgstr "أمر Windows"
msgstr "أمر ويندوز"
#. Disk write
#: src/components/routes/system.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Копирай хоста"
msgid "Copy Linux command"
msgstr "Копирай linux командата"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Копирай име"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Копирай текста"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-14 00:50\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -280,7 +280,7 @@ msgstr "Kopírovat docker compose"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker run command"
msgid "Copy docker run"
msgstr ""
msgstr "Zkopírovat příkaz na spuštění dockeru"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
@@ -296,6 +296,10 @@ msgstr "Kopírovat hostitele"
msgid "Copy Linux command"
msgstr "Kopírovat příkaz Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopírovat název"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopírovat text"
@@ -514,7 +518,7 @@ msgstr "Mřížka"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Homebrew command"
msgstr ""
msgstr "Homebrew příkaz"
#: src/components/add-system.tsx
msgid "Host / IP"
@@ -1092,7 +1096,7 @@ msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Windows command"
msgstr ""
msgstr "Windows příkaz"
#. Disk write
#: src/components/routes/system.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Kopier host"
msgid "Copy Linux command"
msgstr "Kopier Linux kommando"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopier navn"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopier tekst"
@@ -398,7 +402,7 @@ msgstr "Dokumentation"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Down"
msgstr ""
msgstr "Nede"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -407,7 +411,7 @@ msgstr ""
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Edit"
msgstr ""
msgstr "Rediger"
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
@@ -595,7 +599,7 @@ msgstr "Administrer display og notifikationsindstillinger."
#: src/components/add-system.tsx
msgid "Manual setup instructions"
msgstr ""
msgstr "Manuel opsætningsvejledning"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
@@ -703,7 +707,7 @@ msgstr "Adgangskoden skal være på mindst 8 tegn."
#: src/components/login/auth-form.tsx
msgid "Password must be less than 72 bytes."
msgstr ""
msgstr "Adgangskoden skal være mindre end 72 bytes."
#: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received"
@@ -811,7 +815,7 @@ msgstr "Gem indstillinger"
#: src/components/add-system.tsx
msgid "Save system"
msgstr ""
msgstr "Gem system"
#: src/components/navbar.tsx
msgid "Search"
@@ -901,7 +905,7 @@ msgstr "Tabel"
#. Temperature label in systems table
#: src/components/systems-table/systems-table.tsx
msgid "Temp"
msgstr ""
msgstr "Temperatur"
#: src/lib/utils.ts
#: src/components/routes/system.tsx
@@ -1025,7 +1029,7 @@ msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Up"
msgstr ""
msgstr "Oppe"
#: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -27,7 +27,7 @@ msgstr "{0, plural, one {# Tag} other {# Tage}}"
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
msgstr "{0} von {1} Zeile(n) ausgewählt."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
@@ -40,7 +40,7 @@ msgstr "1 Stunde"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr ""
msgstr "1 Min"
#: src/lib/utils.ts
msgid "1 week"
@@ -53,7 +53,7 @@ msgstr "12 Stunden"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr ""
msgstr "15 Min"
#: src/lib/utils.ts
msgid "24 hours"
@@ -66,7 +66,7 @@ msgstr "30 Tage"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "5 min"
msgstr ""
msgstr "5 Min"
#. Table column
#: src/components/systems-table/systems-table.tsx
@@ -77,7 +77,7 @@ msgstr "Aktionen"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
msgstr ""
msgstr "Aktiv"
#: src/components/routes/home.tsx
msgid "Active Alerts"
@@ -116,7 +116,7 @@ msgstr "Agent"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History"
msgstr ""
msgstr "Alarm-Verlauf"
#: src/components/alerts/alert-button.tsx
#: src/components/alerts/alert-button.tsx
@@ -134,7 +134,7 @@ msgstr "Möchtest du {name} wirklich löschen?"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Are you sure?"
msgstr ""
msgstr "Bist du sicher?"
#: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context."
@@ -191,12 +191,12 @@ msgstr "Binär"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr ""
msgstr "Bits (Kbps, Mbps, Gbps)"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr ""
msgstr "Bytes (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
@@ -213,11 +213,11 @@ msgstr "Vorsicht - potenzieller Datenverlust"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr ""
msgstr "Celsius (°C)"
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
msgstr ""
msgstr "Anzeigeeinheiten der Werte ändern."
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
@@ -259,7 +259,7 @@ msgstr "Passwort bestätigen"
#: src/components/routes/home.tsx
msgid "Connection is down"
msgstr ""
msgstr "Verbindung unterbrochen"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -296,6 +296,10 @@ msgstr "Host kopieren"
msgid "Copy Linux command"
msgstr "Linux-Befehl kopieren"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Name kopieren"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Text kopieren"
@@ -329,7 +333,7 @@ msgstr "Konto erstellen"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr ""
msgstr "Erstellt"
#. Dark theme
#: src/components/mode-toggle.tsx
@@ -364,7 +368,7 @@ msgstr "Festplatten-I/O"
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr ""
msgstr "Festplatteneinheit"
#: src/lib/utils.ts
#: src/components/routes/system.tsx
@@ -402,7 +406,7 @@ msgstr "Offline"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
msgstr "Dauer"
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table.tsx
@@ -447,7 +451,7 @@ msgstr "Bestehende Systeme, die nicht in der <0>config.yml</0> definiert sind, w
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
msgstr ""
msgstr "Exportieren"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -459,7 +463,7 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr ""
msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts
msgid "Failed to authenticate"
@@ -486,7 +490,7 @@ msgstr "Filter..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr ""
msgstr "Fingerabdruck"
#: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -548,24 +552,24 @@ msgstr "Hell"
#: src/components/routes/system.tsx
msgid "Load Average"
msgstr ""
msgstr "Durchschnittliche Systemlast"
#: src/lib/utils.ts
msgid "Load Average 15m"
msgstr ""
msgstr "Durchschnittliche Systemlast 15 Min"
#: src/lib/utils.ts
msgid "Load Average 1m"
msgstr ""
msgstr "Durchschnittliche Systemlast 1 Min"
#: src/lib/utils.ts
msgid "Load Average 5m"
msgstr ""
msgstr "Durchschnittliche Systemlast 5 Min"
#. Short label for load average
#: src/components/systems-table/systems-table.tsx
msgid "Load Avg"
msgstr ""
msgstr "Durchschnittliche Last"
#: src/components/navbar.tsx
msgid "Log Out"
@@ -635,7 +639,7 @@ msgstr "Netzwerkverkehr der öffentlichen Schnittstellen"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr ""
msgstr "Netzwerkeinheit"
#: src/components/command-palette.tsx
msgid "No results found."
@@ -643,7 +647,7 @@ msgstr "Keine Ergebnisse gefunden."
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "No results."
msgstr ""
msgstr "Keine Ergebnisse."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
@@ -686,7 +690,7 @@ msgstr "Seite"
#. placeholder {1}: table.getPageCount()
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Page {0} of {1}"
msgstr ""
msgstr "Seite {0} von {1}"
#: src/components/command-palette.tsx
msgid "Pages / Settings"
@@ -786,7 +790,7 @@ msgstr "Passwort zurücksetzen"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Resolved"
msgstr ""
msgstr "Gelöst"
#: src/components/systems-table/systems-table.tsx
msgid "Resume"
@@ -798,7 +802,7 @@ msgstr "Token rotieren"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
msgstr ""
msgstr "Zeilen pro Seite"
#: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
@@ -811,7 +815,7 @@ msgstr "Einstellungen speichern"
#: src/components/add-system.tsx
msgid "Save system"
msgstr ""
msgstr "System sichern"
#: src/components/navbar.tsx
msgid "Search"
@@ -859,7 +863,7 @@ msgstr "Sortieren nach"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
msgid "State"
msgstr ""
msgstr "Status"
#: src/lib/utils.ts
msgid "Status"
@@ -884,7 +888,7 @@ msgstr "System"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
msgstr "Systemlastdurchschnitt im Zeitverlauf"
#: src/components/navbar.tsx
msgid "Systems"
@@ -910,7 +914,7 @@ msgstr "Temperatur"
#: src/components/routes/settings/general.tsx
msgid "Temperature unit"
msgstr ""
msgstr "Temperatureinheit"
#: src/components/routes/system.tsx
msgid "Temperatures of system sensors"
@@ -934,7 +938,7 @@ msgstr "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch werden alle
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "This will permanently delete all selected records from the database."
msgstr ""
msgstr "Dadurch werden alle ausgewählten Datensätze dauerhaft aus der Datenbank gelöscht."
#: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}"
@@ -978,15 +982,15 @@ msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen z
#: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten Minute einen Schwellenwert überschreitet"
#: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr ""
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 15 Minuten einen Schwellenwert überschreitet"
#: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr ""
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwellenwert überschreitet"
#: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1015,7 +1019,7 @@ msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert übersc
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
msgstr ""
msgstr "Einheiten"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
@@ -1025,7 +1029,7 @@ msgstr "Universeller Token"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Up"
msgstr ""
msgstr "aktiv"
#: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information."
@@ -1058,7 +1062,7 @@ msgstr "Benutzer"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr ""
msgstr "Wert"
#: src/components/systems-table/systems-table.tsx
msgid "View"
@@ -1066,7 +1070,7 @@ msgstr "Ansicht"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""
msgstr "Sieh dir die neusten 200 Alarme an."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"

View File

@@ -291,6 +291,10 @@ msgstr "Copy host"
msgid "Copy Linux command"
msgstr "Copy Linux command"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Copy name"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Copy text"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -40,7 +40,7 @@ msgstr "1 hora"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr "1 min"
msgstr ""
#: src/lib/utils.ts
msgid "1 week"
@@ -53,7 +53,7 @@ msgstr "12 horas"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr "15 min"
msgstr ""
#: src/lib/utils.ts
msgid "24 hours"
@@ -66,7 +66,7 @@ msgstr "30 días"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "5 min"
msgstr "5 min"
msgstr ""
#. Table column
#: src/components/systems-table/systems-table.tsx
@@ -191,12 +191,12 @@ msgstr "Binario"
#: 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/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"
@@ -213,7 +213,7 @@ msgstr "Precaución - posible pérdida de datos"
#: 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."
@@ -296,6 +296,10 @@ msgstr "Copiar host"
msgid "Copy Linux command"
msgstr "Copiar comando de Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Copiar nombre"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Copiar texto"
@@ -459,7 +463,7 @@ msgstr "Exporte la configuración actual de sus sistemas."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
msgid "Failed to authenticate"
@@ -960,7 +964,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/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "کپی میزبان"
msgid "Copy Linux command"
msgstr "کپی دستور لینوکس"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "کپی نام"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "کپی متن"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -85,7 +85,7 @@ msgstr "Alertes actives"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Ajouter <0>Système</0>"
msgstr "Ajouter <0>un Système</0>"
#: src/components/add-system.tsx
msgid "Add New System"
@@ -97,7 +97,7 @@ msgstr "Ajouter un système"
#: src/components/routes/settings/notifications.tsx
msgid "Add URL"
msgstr "Ajouter URL"
msgstr "Ajouter lURL"
#: src/components/routes/settings/general.tsx
msgid "Adjust display options for charts."
@@ -296,6 +296,10 @@ msgstr "Copier l'hôte"
msgid "Copy Linux command"
msgstr "Copier la commande Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Copier le nom"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Copier le texte"
@@ -711,7 +715,7 @@ msgstr "Demande de réinitialisation du mot de passe reçue"
#: src/components/systems-table/systems-table.tsx
msgid "Pause"
msgstr "En pause"
msgstr "Pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
@@ -960,7 +964,7 @@ msgstr "Changer le thème"
#: 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/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\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"
@@ -296,6 +296,10 @@ msgstr "Kopiraj hosta"
msgid "Copy Linux command"
msgstr "Kopiraj Linux komandu"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopiraj naziv"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopiraj tekst"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Hoszt másolása"
msgid "Copy Linux command"
msgstr "Linux parancs másolása"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Név másolása"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Szöveg másolása"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: is\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Icelandic\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Afrita host"
msgid "Copy Linux command"
msgstr "Afrita Linux aðgerð"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Afrita nafn"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Afrita texta"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Copia host"
msgid "Copy Linux command"
msgstr "Copia comando Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Copia nome"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Copia testo"
@@ -552,7 +556,7 @@ msgstr ""
#: src/lib/utils.ts
msgid "Load Average 15m"
msgstr ""
msgstr "Caricamento medio 15m"
#: src/lib/utils.ts
msgid "Load Average 1m"
@@ -560,7 +564,7 @@ msgstr ""
#: src/lib/utils.ts
msgid "Load Average 5m"
msgstr ""
msgstr "Caricamento medio 5m"
#. Short label for load average
#: src/components/systems-table/systems-table.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-13 10:13\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -296,6 +296,10 @@ msgstr "ホストをコピー"
msgid "Copy Linux command"
msgstr "Linuxコマンドをコピー"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "名前をコピー"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "テキストをコピー"
@@ -1078,7 +1082,7 @@ msgstr "表示するのに十分なレコードを待っています"
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "翻訳をさらに良くするためにご協力いただけますか?詳細については<0>Crowdin</0>をご覧ください。"
msgstr "翻訳をさらに良くするためにご協力をお願いします。詳細については<0>Crowdin</0>をご覧ください。"
#: src/components/routes/settings/notifications.tsx
msgid "Webhook / Push notifications"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-07 10:06\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -146,7 +146,7 @@ msgstr "평균"
#: src/components/routes/system.tsx
msgid "Average CPU utilization of containers"
msgstr "컨테이너의 평균 CPU 사용량"
msgstr "Docker 컨테이너의 평균 CPU 사용량"
#. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-system.tsx
@@ -274,13 +274,13 @@ msgstr "클립보드에 복사됨"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker compose file content"
msgid "Copy docker compose"
msgstr "docker compose 복사"
msgstr "docker compose 내용 복사"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker run command"
msgid "Copy docker run"
msgstr "docker run 복사"
msgstr "docker run 명령어 복사"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
@@ -296,6 +296,10 @@ msgstr "호스트 복사"
msgid "Copy Linux command"
msgstr "리눅스 명령어 복사"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "이름 복사"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "텍스트 복사"
@@ -715,7 +719,7 @@ msgstr "일시 중지"
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "일시정지됨"
msgstr "일시 정지됨"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -1078,7 +1082,7 @@ msgstr "표시할 충분한 기록을 기다리는 중"
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "번역을 더 좋게 만드는 데 도움을 주시겠습니까? 자세한 내용은 <0>Crowdin</0>을 확인세요."
msgstr "번역을 개선하는데 도움을 주시겠습니까? 자세한 내용은 <0>Crowdin</0>을 확인해 주세요."
#: src/components/routes/settings/notifications.tsx
msgid "Webhook / Push notifications"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -285,7 +285,7 @@ msgstr "Docker run kopiëren"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr ""
msgstr "Env kopiëren"
#: src/components/systems-table/systems-table.tsx
msgid "Copy host"
@@ -296,21 +296,25 @@ msgstr "Kopieer host"
msgid "Copy Linux command"
msgstr "Kopieer Linux-opdracht"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopieer naam"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopieer tekst"
#: 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 ""
msgstr "Kopieer de installatie opdracht voor de agent hieronder, of registreer agenten automatisch met een <0>universele token</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 ""
msgstr "Kopieer de<0>docker-compose.yml</0> inhoud voor de agent hieronder, of registreer agenten automatisch met een <1>universele token</1>."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML"
msgstr ""
msgstr "YAML kopiëren"
#: src/components/systems-table/systems-table.tsx
msgid "CPU"
@@ -352,7 +356,7 @@ msgstr "Verwijderen"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint"
msgstr ""
msgstr "Vingerafdruk verwijderen"
#: src/components/systems-table/systems-table.tsx
msgid "Disk"
@@ -486,7 +490,7 @@ msgstr "Filter..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr ""
msgstr "Vingerafdruk"
#: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -552,7 +556,7 @@ msgstr ""
#: src/lib/utils.ts
msgid "Load Average 15m"
msgstr ""
msgstr "Gemiddelde Belasting 15m"
#: src/lib/utils.ts
msgid "Load Average 1m"
@@ -560,7 +564,7 @@ msgstr ""
#: src/lib/utils.ts
msgid "Load Average 5m"
msgstr ""
msgstr "Gemiddelde Belasting 5m"
#. Short label for load average
#: src/components/systems-table/systems-table.tsx
@@ -794,7 +798,7 @@ msgstr "Hervatten"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr ""
msgstr "Roteer Token"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
@@ -960,21 +964,21 @@ msgstr "Schakel thema"
#: 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/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints"
msgstr ""
msgstr "Tokens & Vingerafdrukken"
#: 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 ""
msgstr "Tokens staan agenten toe om verbinding te maken met en te registreren. Vingerafdrukken zijn stabiele Ids deze zijn uniek voor elk systeem, ingesteld bij eerste verbinding."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
msgstr "Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub."
#: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
@@ -982,11 +986,11 @@ msgstr ""
#: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr ""
msgstr "Triggert wanneer de 15 minuten gemiddelde belasting een drempelwaarde overschrijdt"
#: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr ""
msgstr "Triggert wanneer de 5 minuten gemiddelde belasting een drempelwaarde overschrijdt"
#: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1019,7 +1023,7 @@ msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
msgstr "Universele token"
#. Context: System is up
#: src/components/systems-table/systems-table.tsx
@@ -1086,7 +1090,7 @@ msgstr "Webhook / Pushmeldingen"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr ""
msgstr "Wanneer ingeschakeld kunnen agenten zich met dit token registreren zonder dat er vooraf een systeem aangemaakt hoeft te worden. Het token verloopt na één uur of bij herstart van de hub."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Kopier vert"
msgid "Copy Linux command"
msgstr "Kopier Linux-kommando"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopier navn"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopier tekst"
@@ -715,7 +719,7 @@ msgstr "Pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pauset"
msgstr "Satt på Pause"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\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"
@@ -296,6 +296,10 @@ msgstr "Kopiuj host"
msgid "Copy Linux command"
msgstr "Kopiuj polecenie Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopiuj nazwę"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopiuj tekst"
@@ -398,7 +402,7 @@ msgstr "Dokumentacja"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Down"
msgstr ""
msgstr "Nie działa"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -407,7 +411,7 @@ msgstr ""
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Edit"
msgstr ""
msgstr "Edytuj"
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
@@ -595,7 +599,7 @@ msgstr "Zarządzaj preferencjami wyświetlania i powiadomień."
#: src/components/add-system.tsx
msgid "Manual setup instructions"
msgstr ""
msgstr "Instrukcja ręcznej konfiguracji"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
@@ -703,7 +707,7 @@ msgstr "Hasło musi mieć co najmniej 8 znaków."
#: src/components/login/auth-form.tsx
msgid "Password must be less than 72 bytes."
msgstr ""
msgstr "Hasło musi być mniejsze niż 72 bajty."
#: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received"
@@ -811,7 +815,7 @@ msgstr "Zapisz ustawienia"
#: src/components/add-system.tsx
msgid "Save system"
msgstr ""
msgstr "Zapisz system"
#: src/components/navbar.tsx
msgid "Search"
@@ -901,7 +905,7 @@ msgstr "Tabela"
#. Temperature label in systems table
#: src/components/systems-table/systems-table.tsx
msgid "Temp"
msgstr ""
msgstr "Temperatura"
#: src/lib/utils.ts
#: src/components/routes/system.tsx
@@ -1025,7 +1029,7 @@ msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Up"
msgstr ""
msgstr "Działa"
#: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Copiar host"
msgid "Copy Linux command"
msgstr "Copiar comando Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Copiar nome"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Copiar texto"
@@ -398,7 +402,7 @@ msgstr "Documentação"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Down"
msgstr ""
msgstr "“Desligado”"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -595,7 +599,7 @@ msgstr "Gerenciar preferências de exibição e notificação."
#: src/components/add-system.tsx
msgid "Manual setup instructions"
msgstr ""
msgstr "Instruções de configuração manual"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
@@ -960,7 +964,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/tokens-fingerprints.tsx
@@ -1025,7 +1029,7 @@ msgstr "Token universal"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Up"
msgstr ""
msgstr "“Ligado”"
#: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\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"
@@ -27,7 +27,7 @@ msgstr "{0, plural, one {# день} other {# дней}}"
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
msgstr "Выбрано {0} из {1} строк."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
@@ -40,7 +40,7 @@ msgstr "1 час"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr ""
msgstr "1 мин"
#: src/lib/utils.ts
msgid "1 week"
@@ -53,7 +53,7 @@ msgstr "12 часов"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr ""
msgstr "15 мин"
#: src/lib/utils.ts
msgid "24 hours"
@@ -66,7 +66,7 @@ msgstr "30 дней"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "5 min"
msgstr ""
msgstr "5 мин"
#. Table column
#: src/components/systems-table/systems-table.tsx
@@ -77,7 +77,7 @@ msgstr "Действия"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
msgstr ""
msgstr "Активно"
#: src/components/routes/home.tsx
msgid "Active Alerts"
@@ -116,7 +116,7 @@ msgstr "Агент"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History"
msgstr ""
msgstr "История оповещений"
#: src/components/alerts/alert-button.tsx
#: src/components/alerts/alert-button.tsx
@@ -134,7 +134,7 @@ msgstr "Вы уверены, что хотите удалить {name}?"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Are you sure?"
msgstr ""
msgstr "Вы уверены?"
#: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context."
@@ -191,12 +191,12 @@ msgstr "Двоичный"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr ""
msgstr "Биты (Кбит/с, Мбит/с, Гбит/с)"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr ""
msgstr "Байты (Кбайт/с, Мбайт/с, Гбайт/с)"
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
@@ -213,11 +213,11 @@ msgstr "Внимание - возможная потеря данных"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr ""
msgstr "Цельсий (°C)"
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
msgstr ""
msgstr "Изменить единицы измерения для метрик."
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
@@ -259,7 +259,7 @@ msgstr "Подтвердите пароль"
#: src/components/routes/home.tsx
msgid "Connection is down"
msgstr ""
msgstr "Нет соединения"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -285,7 +285,7 @@ msgstr "Скопировать docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr ""
msgstr "Скопировать env"
#: src/components/systems-table/systems-table.tsx
msgid "Copy host"
@@ -296,21 +296,25 @@ msgstr "Копировать хост"
msgid "Copy Linux command"
msgstr "Копировать команду Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Копировать имя"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
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 ""
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 ""
msgstr "Скопируйте содержимое <0>docker-compose.yml</0> для агента ниже, или зарегистрируйте агентов автоматически с помощью <1>универсального токена</1>."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML"
msgstr ""
msgstr "Скопировать YAML"
#: src/components/systems-table/systems-table.tsx
msgid "CPU"
@@ -329,7 +333,7 @@ msgstr "Создать аккаунт"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr ""
msgstr "Создано"
#. Dark theme
#: src/components/mode-toggle.tsx
@@ -352,7 +356,7 @@ msgstr "Удалить"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint"
msgstr ""
msgstr "Удалить отпечаток"
#: src/components/systems-table/systems-table.tsx
msgid "Disk"
@@ -364,7 +368,7 @@ msgstr "Дисковый ввод/вывод"
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr ""
msgstr "Единицы измерения температуры"
#: src/lib/utils.ts
#: src/components/routes/system.tsx
@@ -402,7 +406,7 @@ msgstr "Не в сети"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
msgstr "Длительность"
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table.tsx
@@ -447,7 +451,7 @@ msgstr "Существующие системы, не определенные
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
msgstr ""
msgstr "Экспорт"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -459,7 +463,7 @@ msgstr "Экспортируйте текущую конфигурацию си
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr ""
msgstr "Фаренгейт (°F)"
#: src/lib/utils.ts
msgid "Failed to authenticate"
@@ -486,7 +490,7 @@ msgstr "Фильтр..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr ""
msgstr "Отпечаток"
#: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -548,24 +552,24 @@ msgstr "Светлая"
#: src/components/routes/system.tsx
msgid "Load Average"
msgstr ""
msgstr "Средняя загрузка"
#: src/lib/utils.ts
msgid "Load Average 15m"
msgstr ""
msgstr "Средняя загрузка за 15м"
#: src/lib/utils.ts
msgid "Load Average 1m"
msgstr ""
msgstr "Средняя загрузка за 1м"
#: src/lib/utils.ts
msgid "Load Average 5m"
msgstr ""
msgstr "Средняя загрузка за 5м"
#. Short label for load average
#: src/components/systems-table/systems-table.tsx
msgid "Load Avg"
msgstr ""
msgstr "Ср. загрузка"
#: src/components/navbar.tsx
msgid "Log Out"
@@ -635,7 +639,7 @@ msgstr "Сетевой трафик публичных интерфейсов"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr ""
msgstr "Единицы измерения скорости сети"
#: src/components/command-palette.tsx
msgid "No results found."
@@ -643,7 +647,7 @@ msgstr "Результаты не найдены."
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "No results."
msgstr ""
msgstr "Нет результатов."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
@@ -686,7 +690,7 @@ msgstr "Страница"
#. placeholder {1}: table.getPageCount()
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Page {0} of {1}"
msgstr ""
msgstr "Страница {0} из {1}"
#: src/components/command-palette.tsx
msgid "Pages / Settings"
@@ -715,7 +719,7 @@ msgstr "Пауза"
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Приостановлено"
msgstr "Пауза"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -786,7 +790,7 @@ msgstr "Сбросить пароль"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Resolved"
msgstr ""
msgstr "Завершено"
#: src/components/systems-table/systems-table.tsx
msgid "Resume"
@@ -794,11 +798,11 @@ msgstr "Возобновить"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr ""
msgstr "Обновить токен"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
msgstr ""
msgstr "Строк на странице"
#: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
@@ -859,7 +863,7 @@ msgstr "Сортировать по"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
msgid "State"
msgstr ""
msgstr "Состояние"
#: src/lib/utils.ts
msgid "Status"
@@ -884,7 +888,7 @@ msgstr "Система"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
msgstr "Средняя загрузка системы за время"
#: src/components/navbar.tsx
msgid "Systems"
@@ -910,7 +914,7 @@ msgstr "Температура"
#: src/components/routes/settings/general.tsx
msgid "Temperature unit"
msgstr ""
msgstr "Температура"
#: src/components/routes/system.tsx
msgid "Temperatures of system sensors"
@@ -934,7 +938,7 @@ msgstr "Это действие не может быть отменено. Эт
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "This will permanently delete all selected records from the database."
msgstr ""
msgstr "Это навсегда удалит все выбранные записи из базы данных."
#: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}"
@@ -960,33 +964,33 @@ msgstr "Переключить тему"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr ""
msgstr "Токен"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints"
msgstr ""
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 ""
msgstr "Токены позволяют агентам подключаться и регистрироваться. Отпечатки являются постоянными идентификаторами, уникальными для каждой системы, которые создаются при первом подключении."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
msgstr "Токены и отпечатки используются для аутентификации соединений WebSocket с хабом."
#: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
msgstr "Срабатывает, когда средняя загрузка за 1 минуту превышает порог"
#: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr ""
msgstr "Срабатывает, когда средняя загрузка за 15 минут превышает порог"
#: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr ""
msgstr "Срабатывает, когда средняя загрузка за 5 минут превышает порог"
#: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1015,11 +1019,11 @@ msgstr "Срабатывает, когда использование любог
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
msgstr ""
msgstr "Параметры единиц измерения"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
msgstr "Универсальный токен"
#. Context: System is up
#: src/components/systems-table/systems-table.tsx
@@ -1058,7 +1062,7 @@ msgstr "Пользователи"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr ""
msgstr "Значение"
#: src/components/systems-table/systems-table.tsx
msgid "View"
@@ -1066,7 +1070,7 @@ msgstr "Вид"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""
msgstr "Просмотреть 200 последних оповещений."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
@@ -1086,7 +1090,7 @@ msgstr "Webhook / Push уведомления"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr ""
msgstr "Если включено, этот токен позволяет агентам регистрироваться самостоятельно без предварительного создания системы. Истекает через час или при перезапуске хаба."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\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"
@@ -296,6 +296,10 @@ msgstr "Kopiraj gostitelja"
msgid "Copy Linux command"
msgstr "Kopiraj Linux ukaz"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopiraj ime"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopiraj besedilo"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sv\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-08-01 23:21\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -27,7 +27,7 @@ msgstr "{0, plural, one {# dag} other {# dagar}}"
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
msgstr "{0} av {1} rad(er) valda."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
@@ -40,7 +40,7 @@ msgstr "1 timme"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr ""
msgstr "1 min"
#: src/lib/utils.ts
msgid "1 week"
@@ -53,7 +53,7 @@ msgstr "12 timmar"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr ""
msgstr "15 min"
#: src/lib/utils.ts
msgid "24 hours"
@@ -66,7 +66,7 @@ msgstr "30 dagar"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "5 min"
msgstr ""
msgstr "5 min"
#. Table column
#: src/components/systems-table/systems-table.tsx
@@ -77,7 +77,7 @@ msgstr "Åtgärder"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
msgstr ""
msgstr "Aktiv"
#: src/components/routes/home.tsx
msgid "Active Alerts"
@@ -134,7 +134,7 @@ msgstr "Är du säker på att du vill ta bort {name}?"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Are you sure?"
msgstr ""
msgstr "Är du säker?"
#: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context."
@@ -191,12 +191,12 @@ msgstr "Binär"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr ""
msgstr "Bits (Kbps, Mbps, Gbps)"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr ""
msgstr "Bytes (KB/s, MB/s, GB/S)"
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
@@ -213,11 +213,11 @@ msgstr "Varning - potentiell dataförlust"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr ""
msgstr "Celsius (°C)"
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
msgstr ""
msgstr "Ändra enheter för mätvärden."
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
@@ -259,7 +259,7 @@ msgstr "Bekräfta lösenord"
#: src/components/routes/home.tsx
msgid "Connection is down"
msgstr ""
msgstr "Ej ansluten"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -296,6 +296,10 @@ msgstr "Kopiera värd"
msgid "Copy Linux command"
msgstr "Kopiera Linux-kommando"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Kopiera namn"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Kopiera text"
@@ -310,7 +314,7 @@ msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML"
msgstr ""
msgstr "Kopiera YAML"
#: src/components/systems-table/systems-table.tsx
msgid "CPU"
@@ -329,7 +333,7 @@ msgstr "Skapa konto"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr ""
msgstr "Skapad"
#. Dark theme
#: src/components/mode-toggle.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: tr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -296,6 +296,10 @@ msgstr "Ana bilgisayarı kopyala"
msgid "Copy Linux command"
msgstr "Linux komutunu kopyala"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Adı kopyala"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Metni kopyala"
@@ -960,7 +964,7 @@ msgstr "Temayı değiştir"
#: 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/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: uk\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-15 04:14\n"
"PO-Revision-Date: 2025-07-25 22:44\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"
@@ -285,7 +285,7 @@ msgstr "Копіювати docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr "Копіювати змінні середовища"
msgstr "Копіювати env"
#: src/components/systems-table/systems-table.tsx
msgid "Copy host"
@@ -296,6 +296,10 @@ msgstr "Копіювати хост"
msgid "Copy Linux command"
msgstr "Копіювати команду Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Копіювати імʼя"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Копіювати текст"
@@ -1078,7 +1082,7 @@ msgstr "Очікування достатньої кількості запис
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "Хочете допомогти нам зробити наші переклади ще кращими? Перегляньте <0>Crowdin</0> для отримання додаткової інформації."
msgstr "Хочете допомогти покращити наші переклади? Подробиці на <0>Crowdin</0>."
#: src/components/routes/settings/notifications.tsx
msgid "Webhook / Push notifications"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: vi\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Vietnamese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -296,6 +296,10 @@ msgstr "Sao chép máy chủ"
msgid "Copy Linux command"
msgstr "Sao chép lệnh Linux"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "Sao chép tên"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "Sao chép văn bản"
@@ -960,7 +964,7 @@ msgstr "Chuyển đổi chủ đề"
#: 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/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -296,6 +296,10 @@ msgstr "复制主机名"
msgid "Copy Linux command"
msgstr "复制 Linux 安装命令"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "复制名称"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "复制文本"
@@ -398,7 +402,7 @@ msgstr "文档"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Down"
msgstr "停止"
msgstr "离线"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -552,7 +556,7 @@ msgstr "系统负载"
#: src/lib/utils.ts
msgid "Load Average 15m"
msgstr "15分钟负载平均值"
msgstr "15分钟内的平均负载"
#: src/lib/utils.ts
msgid "Load Average 1m"
@@ -560,7 +564,7 @@ msgstr "1分钟负载平均值"
#: src/lib/utils.ts
msgid "Load Average 5m"
msgstr "5分钟负载平均值"
msgstr "5分钟内的平均负载"
#. Short label for load average
#: src/components/systems-table/systems-table.tsx
@@ -986,7 +990,7 @@ msgstr "当15分钟负载平均值超过阈值时触发"
#: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "当5分钟负载平均值超过阈值时触发"
msgstr "当5分钟内的平均负载超过阈值时触发"
#: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1025,7 +1029,7 @@ msgstr "通用令牌"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Up"
msgstr "启动"
msgstr "在线"
#: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -296,6 +296,10 @@ msgstr "複製主機"
msgid "Copy Linux command"
msgstr "複製 Linux 指令"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "複製名稱"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "複製文本"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-06 07:27\n"
"PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -274,13 +274,13 @@ msgstr "已複製到剪貼簿"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker compose file content"
msgid "Copy docker compose"
msgstr "复制 docker compose"
msgstr "複製 docker compose"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy docker run command"
msgid "Copy docker run"
msgstr "复制 docker run"
msgstr "複製 docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
@@ -296,6 +296,10 @@ msgstr "複製主機"
msgid "Copy Linux command"
msgstr "複製 Linux 指令"
#: src/components/systems-table/systems-table.tsx
msgid "Copy name"
msgstr "複製名稱"
#: src/components/copy-to-clipboard.tsx
msgid "Copy text"
msgstr "複製文字"
@@ -398,7 +402,7 @@ msgstr "文件"
#: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
msgid "Down"
msgstr "中斷"
msgstr "離線"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
@@ -514,7 +518,7 @@ msgstr "網格"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Homebrew command"
msgstr "Homebrew 令"
msgstr "Homebrew 令"
#: src/components/add-system.tsx
msgid "Host / IP"
@@ -715,7 +719,7 @@ msgstr "暫停"
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "已停"
msgstr "已停"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -794,7 +798,7 @@ msgstr "繼續"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr "輪令牌"
msgstr "輪令牌"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
@@ -970,11 +974,11 @@ 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 "令牌允許代理程式連和註冊。指紋是每個系統唯一穩定識別符,在首次連接時設置。"
msgstr "令牌允許代理程式連和註冊。指紋是每個系統唯一穩定識別符,在首次連線時產生。"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌和指紋用於驗證到中心的WebSocket連接。"
msgstr "令牌和指紋用於驗證到 Hub 的 WebSocket 連線。"
#: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
@@ -1078,7 +1082,7 @@ msgstr "等待足夠的記錄以顯示"
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "想幫助我們改善翻譯嗎?查看<0>Crowdin</0>以取得更多詳細信息。"
msgstr "想幫助我們改善翻譯嗎?查看<0>Crowdin</0>以取得更多詳細資訊。"
#: src/components/routes/settings/notifications.tsx
msgid "Webhook / Push notifications"
@@ -1086,13 +1090,13 @@ msgstr "Webhook / 推送通知"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr "啟用時,此令牌允許代理程式在無需預先創建系統的情況下自動註冊。在一小時後或中心重啟時過期。"
msgstr "啟用時,此令牌允許代理程式在沒有預先建立系統的情況下自動註冊。在一小時後或 Hub 重啟時過期。"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "Windows command"
msgstr "Windows 令"
msgstr "Windows 令"
#. Disk write
#: src/components/routes/system.tsx

View File

@@ -50,6 +50,8 @@ export interface SystemInfo {
l5?: number
/** load average 15 minutes */
l15?: number
/** load average */
la?: [number, number, number]
/** operating system */
o?: string
/** uptime */
@@ -79,12 +81,15 @@ export interface SystemStats {
cpu: number
/** peak cpu */
cpum?: number
// TODO: remove these in future release in favor of la
/** load average 1 minute */
l1?: number
/** load average 5 minutes */
l5?: number
/** load average 15 minutes */
l15?: number
/** load average */
la?: [number, number, number]
/** total memory (gb) */
m: number
/** memory used (gb) */
@@ -219,13 +224,14 @@ export interface ChartTimeData {
}
export interface UserSettings {
// lang?: string
chartTime: ChartTimes
emails?: string[]
webhooks?: string[]
unitTemp?: Unit
unitNet?: Unit
unitDisk?: Unit
colorWarn?: number
colorCrit?: number
}
type ChartDataContainer = {
@@ -234,7 +240,14 @@ type ChartDataContainer = {
[key: string]: key extends "created" ? never : ContainerStats
}
export interface SemVer {
major: number
minor: number
patch: number
}
export interface ChartData {
agentVersion: SemVer
systemStats: SystemStatsRecord[]
containerData: ChartDataContainer[]
orientation: "right" | "left"

View File

@@ -3,7 +3,7 @@ package beszel
import "github.com/blang/semver"
const (
Version = "0.12.0"
Version = "0.12.2"
AppName = "beszel"
)

View File

@@ -16,7 +16,7 @@ It has a friendly web interface, simple configuration, and is ready to use out o
- **Lightweight**: Smaller and less resource-intensive than leading solutions.
- **Simple**: Easy setup with little manual configuration required.
- **Docker stats**: Tracks CPU, memory, and network usage history for each container.
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, and status.
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, load average, and status.
- **Multi-user**: Users manage their own systems. Admins can share systems across users.
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
- **Automatic backups**: Save to and restore from disk or S3-compatible storage.

View File

@@ -31,10 +31,12 @@ if ! getent passwd "$SERVICE_USER" >/dev/null; then
--gecos "System user for $SERVICE"
fi
# Enable docker
if ! getent group docker | grep -q "$SERVICE_USER"; then
echo "Adding $SERVICE_USER to docker group"
usermod -aG docker "$SERVICE_USER"
# Enable docker (only if docker group exists)
if getent group docker >/dev/null 2>&1; then
if ! getent group docker | grep -q "$SERVICE_USER"; then
echo "Adding $SERVICE_USER to docker group"
usermod -aG docker "$SERVICE_USER"
fi
fi
# Create config file if it doesn't already exist

View File

@@ -1,9 +1,11 @@
services:
beszel-agent:
image: 'henrygd/beszel-agent'
image: 'henrygd/beszel-agent' #Or henrygd/beszel-agent-nvidia
container_name: 'beszel-agent'
restart: unless-stopped
network_mode: host
# Only when using henrygd/beszel-agent-nvidia
# runtime: nvidia
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# monitor other disks / partitions by mounting a folder in /extra-filesystems
@@ -11,3 +13,6 @@ services:
environment:
PORT: 45876
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
# Only when using henrygd/beszel-agent-nvidia
# NVIDIA_VISIBLE_DEVICES: all
# NVIDIA_DRIVER_CAPABILITIES: compute,video,utility

View File

@@ -11,13 +11,16 @@ services:
- 'host.docker.internal:host-gateway'
beszel-agent:
image: 'henrygd/beszel-agent'
image: 'henrygd/beszel-agent' #Add -nvidia for nvidia gpus
container_name: 'beszel-agent'
restart: unless-stopped
network_mode: host
# runtime: nvidia # when using beszel-agent-nvidia
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
KEY: '...'
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats
# NVIDIA_VISIBLE_DEVICES: all # when using beszel-agent-nvidia
# NVIDIA_DRIVER_CAPABILITIES: utility # when using beszel-agent-nvidia

View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -5,7 +5,7 @@ is_alpine() {
}
is_openwrt() {
cat /etc/os-release | grep -q "OpenWrt"
grep -qi "OpenWrt" /etc/os-release
}
# If SELinux is enabled, set the context of the binary
@@ -227,8 +227,8 @@ if [ "$UNINSTALL" = true ]; then
rm -f /var/log/beszel-agent.log /var/log/beszel-agent.err
elif is_openwrt; then
echo "Stopping and disabling the agent service..."
service beszel-agent stop
service beszel-agent disable
/etc/init.d/beszel-agent stop
/etc/init.d/beszel-agent disable
echo "Removing the OpenWRT service files..."
rm -f /etc/init.d/beszel-agent
@@ -288,13 +288,13 @@ package_installed() {
}
# Check for package manager and install necessary packages if not installed
if is_alpine; then
if ! package_installed tar || ! package_installed curl || ! package_installed coreutils; then
if package_installed apk; then
if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then
apk update
apk add tar curl coreutils shadow
fi
elif is_openwrt; then
if ! package_installed tar || ! package_installed curl || ! package_installed coreutils; then
elif package_installed opkg; then
if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then
opkg update
opkg install tar curl coreutils
fi
@@ -335,11 +335,10 @@ else
fi
# Create a dedicated user for the service if it doesn't exist
echo "Creating a dedicated user for the Beszel Agent service..."
if is_alpine; then
if ! id -u beszel >/dev/null 2>&1; then
echo "Creating a dedicated group for the Beszel Agent service..."
addgroup beszel
echo "Creating a dedicated user for the Beszel Agent service..."
adduser -S -D -H -s /sbin/nologin -G beszel beszel
fi
# Add the user to the docker group to allow access to the Docker socket if group docker exists
@@ -347,10 +346,37 @@ if is_alpine; then
echo "Adding beszel to docker group"
usermod -aG docker beszel
fi
elif is_openwrt; then
# Create beszel group first if it doesn't exist (check /etc/group directly)
if ! grep -q "^beszel:" /etc/group >/dev/null 2>&1; then
echo "beszel:x:999:" >> /etc/group
fi
# Create beszel user if it doesn't exist (double-check to prevent duplicates)
if ! id -u beszel >/dev/null 2>&1 && ! grep -q "^beszel:" /etc/passwd >/dev/null 2>&1; then
echo "beszel:x:999:999::/nonexistent:/bin/false" >> /etc/passwd
fi
# Add the user to the docker group if docker group exists and user is not already in it
if grep -q "^docker:" /etc/group >/dev/null 2>&1; then
echo "Adding beszel to docker group"
# Check if beszel is already in docker group
if ! grep "^docker:" /etc/group | grep -q "beszel"; then
# Add beszel to docker group by modifying /etc/group
# Handle both cases: group with existing members and group without members
if grep "^docker:" /etc/group | grep -q ":.*:.*$"; then
# Group has existing members, append with comma
sed -i 's/^docker:\([^:]*:[^:]*:\)\(.*\)$/docker:\1\2,beszel/' /etc/group
else
# Group has no members, just append
sed -i 's/^docker:\([^:]*:[^:]*:\)$/docker:\1beszel/' /etc/group
fi
fi
fi
else
if ! id -u beszel >/dev/null 2>&1; then
echo "Creating a dedicated user for the Beszel Agent service..."
useradd --system --home-dir /nonexistent --shell /bin/false beszel
fi
# Add the user to the docker group to allow access to the Docker socket if group docker exists
@@ -427,6 +453,11 @@ set_selinux_context
# Cleanup
rm -rf "$TEMP_DIR"
# Make sure /etc/machine-id exists for persistent fingerprint
if [ ! -f /etc/machine-id ]; then
cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id
fi
# Check for NVIDIA GPUs and grant device permissions for systemd service
detect_nvidia_devices() {
local devices=""
@@ -546,10 +577,7 @@ start_service() {
procd_set_param command /opt/beszel-agent/beszel-agent
procd_set_param user beszel
procd_set_param pidfile /var/run/beszel-agent.pid
procd_set_param env PORT="$PORT"
procd_set_param env KEY="$KEY"
procd_set_param env TOKEN="$TOKEN"
procd_set_param env HUB_URL="$HUB_URL"
procd_set_param env PORT="$PORT" KEY="$KEY" TOKEN="$TOKEN" HUB_URL="$HUB_URL"
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
@@ -573,10 +601,10 @@ EOF
# Enable the service
chmod +x /etc/init.d/beszel-agent
service beszel-agent enable
/etc/init.d/beszel-agent enable
# Start the service
service beszel-agent restart
/etc/init.d/beszel-agent restart
# Auto-update service for OpenWRT using a crontab job
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
@@ -604,9 +632,9 @@ EOF
esac
# Check service status
if ! service beszel-agent running >/dev/null 2>&1; then
if ! /etc/init.d/beszel-agent running >/dev/null 2>&1; then
echo "Error: The Beszel Agent service is not running."
service beszel-agent status
/etc/init.d/beszel-agent status
exit 1
fi

View File

@@ -217,13 +217,6 @@ function Update-ServicePath {
throw "NSSM is not available in PATH and no valid NSSMPath was provided"
}
# Stop the service
Write-Host "Stopping beszel-agent service..."
& $nssmCommand stop beszel-agent
if ($LASTEXITCODE -ne 0) {
Write-Host "Warning: Failed to stop service, continuing anyway..." -ForegroundColor Yellow
}
# Update the application path
& $nssmCommand set beszel-agent Application $NewAgentPath
if ($LASTEXITCODE -ne 0) {
@@ -233,7 +226,25 @@ function Update-ServicePath {
Write-Host "Service path updated to: $NewAgentPath"
# Start the service
Start-BeszelAgentService -NSSMPath $nssmCommand
}
# Function to start and monitor the service
function Start-BeszelAgentService {
param (
[string]$NSSMPath = ""
)
Write-Host "Starting beszel-agent service..."
# Determine the NSSM executable to use
$nssmCommand = "nssm"
if ($NSSMPath -and (Test-Path $NSSMPath)) {
$nssmCommand = $NSSMPath
} elseif (-not (Test-CommandExists "nssm")) {
throw "NSSM is not available in PATH and no valid NSSMPath was provided"
}
& $nssmCommand start beszel-agent
$startResult = $LASTEXITCODE
@@ -255,7 +266,7 @@ function Update-ServicePath {
if ($serviceStatus -eq "SERVICE_RUNNING") {
$serviceStarted = $true
Write-Host "Success! The beszel-agent service is now running with the updated path." -ForegroundColor Green
Write-Host "Success! The beszel-agent service is now running." -ForegroundColor Green
}
elseif ($serviceStatus -like "*PENDING*") {
Write-Host "Service is still starting (status: $serviceStatus)... waiting" -ForegroundColor Yellow
@@ -273,7 +284,7 @@ function Update-ServicePath {
}
} else {
# NSSM start command was successful
Write-Host "Success! The beszel-agent service is running with the updated path." -ForegroundColor Green
Write-Host "Success! The beszel-agent service is running properly." -ForegroundColor Green
}
}
@@ -287,7 +298,6 @@ $isAdmin = Test-Admin
try {
Write-Host "Beszel Agent Upgrade Script" -ForegroundColor Cyan
Write-Host "===========================" -ForegroundColor Cyan
Write-Host ""
# First: Check if service exists (doesn't require admin)
$existingService = Get-Service -Name "beszel-agent" -ErrorAction SilentlyContinue
@@ -312,6 +322,13 @@ try {
Write-Host "Retrieving current service configuration..."
$currentConfig = Get-ServiceConfiguration -NSSMPath $nssmPath
# Stop the service before upgrade
Write-Host "Stopping beszel-agent service..."
& $nssmPath stop beszel-agent
if ($LASTEXITCODE -ne 0) {
Write-Host "Warning: Failed to stop service, continuing anyway..." -ForegroundColor Yellow
}
# Upgrade the agent (doesn't require admin)
Write-Host "Upgrading beszel-agent..."
$newAgentPath = $null
@@ -340,7 +357,8 @@ try {
# Check if the path has changed
if ($currentConfig.CurrentPath -eq $newAgentPath) {
Write-Host "Agent path has not changed. Service update not needed." -ForegroundColor Green
Write-Host "Agent path has not changed. Restarting service..." -ForegroundColor Green
Start-BeszelAgentService -NSSMPath $nssmPath
Write-Host "Upgrade completed successfully!" -ForegroundColor Green
exit 0
}