mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 05:56:17 +01:00
Compare commits
20 Commits
v0.12.1
...
custom-col
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df9e2dec28 | ||
|
|
a0f271545a | ||
|
|
aa2bc9f118 | ||
|
|
b22ae87022 | ||
|
|
79e79079bc | ||
|
|
1811ebdee4 | ||
|
|
137f3f3e24 | ||
|
|
ed1d1e77c0 | ||
|
|
8c36dd1caa | ||
|
|
57bfe72486 | ||
|
|
75f66b0246 | ||
|
|
ce93d54aa7 | ||
|
|
39dbe0eac5 | ||
|
|
7282044f80 | ||
|
|
d77c37c0b0 | ||
|
|
e362cbbca5 | ||
|
|
118544926b | ||
|
|
d4bb0a0a30 | ||
|
|
fe5e35d1a9 | ||
|
|
60a6ae2caa |
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -17,3 +17,5 @@ beszel/build
|
||||
beszel/site/src/locales/**/*.ts
|
||||
*.bak
|
||||
__debug_*
|
||||
beszel/internal/agent/lhm/obj
|
||||
beszel/internal/agent/lhm/bin
|
||||
|
||||
@@ -202,13 +202,14 @@ winget:
|
||||
owner: henrygd
|
||||
name: beszel-winget
|
||||
branch: henrygd.beszel-agent-{{ .Version }}
|
||||
pull_request:
|
||||
enabled: true
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
80
beszel/internal/agent/lhm/beszel_lhm.cs
Normal file
80
beszel/internal/agent/lhm/beszel_lhm.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
beszel/internal/agent/lhm/beszel_lhm.csproj
Normal file
11
beszel/internal/agent/lhm/beszel_lhm.csproj
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
9
beszel/internal/agent/sensors_default.go
Normal file
9
beszel/internal/agent/sensors_default.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
var getSensorTemps = sensors.TemperaturesWithContext
|
||||
281
beszel/internal/agent/sensors_windows.go
Normal file
281
beszel/internal/agent/sensors_windows.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
@@ -42,29 +43,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
|
||||
// intialize settings with defaults (zero values can be ignored)
|
||||
settings := UserSettings{
|
||||
ChartTime: "1h",
|
||||
NotificationEmails: []string{},
|
||||
NotificationWebhooks: []string{},
|
||||
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"`
|
||||
}
|
||||
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.NotificationEmails = []string{user.Email}
|
||||
if len(settings.NotificationWebhooks) == 0 {
|
||||
settings.NotificationWebhooks = []string{""}
|
||||
}
|
||||
// if len(settings.NotificationWebhooks) == 0 {
|
||||
// settings.NotificationWebhooks = []string{""}
|
||||
// }
|
||||
record.Set("settings", settings)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
4
beszel/site/package-lock.json
generated
4
beszel/site/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "beszel",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"dependencies": {
|
||||
"@henrygd/queue": "^1.0.7",
|
||||
"@henrygd/semaphore": "^0.0.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"private": true,
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -87,5 +87,5 @@ export default function AreaChartDefault({
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}, [chartData.systemStats.length, yAxisWidth, maxToggled])
|
||||
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
|
||||
}
|
||||
|
||||
@@ -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("")]
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -11,17 +11,30 @@ 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 { setLang } from "@/lib/i18n"
|
||||
import { Unit } from "@/lib/enums"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { i18n } = useLingui()
|
||||
|
||||
// Remove all per-metric threshold state and UI
|
||||
// Only keep general yellow/red threshold state and UI
|
||||
const [yellow, setYellow] = useState(userSettings.meterThresholds?.yellow ?? 65)
|
||||
const [red, setRed] = useState(userSettings.meterThresholds?.red ?? 90)
|
||||
|
||||
function handleResetThresholds() {
|
||||
setYellow(65)
|
||||
setRed(90)
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const data = Object.fromEntries(formData) as Partial<UserSettings>
|
||||
data.meterThresholds = { yellow, red }
|
||||
await saveSettings(data)
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -101,6 +114,45 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
<Trans>Dashboard meter thresholds</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Choose when the dashboard meters changes colors, based on percentage values.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 items-end">
|
||||
<div>
|
||||
<Label htmlFor="yellow-threshold"><Trans>Warning threshold (%)</Trans></Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="yellow-threshold"
|
||||
min={1}
|
||||
max={100}
|
||||
value={yellow}
|
||||
onChange={e => setYellow(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="red-threshold"><Trans>Danger threshold (%)</Trans></Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="red-threshold"
|
||||
min={1}
|
||||
max={100}
|
||||
value={red}
|
||||
onChange={e => setRed(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={handleResetThresholds} disabled={isLoading} className="mt-4">
|
||||
<Trans>Reset to default</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
{/* Unit preferences section fixed and wrapped in a div */}
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
@@ -133,7 +185,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 +207,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>
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
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,
|
||||
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"
|
||||
|
||||
/**
|
||||
* @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: CellFormatter,
|
||||
Icon: CpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
// accessorKey: "info.mp",
|
||||
accessorFn: ({ info }) => info.mp,
|
||||
id: "memory",
|
||||
name: () => t`Memory`,
|
||||
cell: CellFormatter,
|
||||
Icon: MemoryStickIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.dp,
|
||||
id: "disk",
|
||||
name: () => t`Disk`,
|
||||
cell: CellFormatter,
|
||||
Icon: HardDriveIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.g,
|
||||
id: "gpu",
|
||||
name: () => "GPU",
|
||||
cell: CellFormatter,
|
||||
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
|
||||
}
|
||||
|
||||
function getDotColor() {
|
||||
const normalized = max / (sysInfo.t ?? 1)
|
||||
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: ({ 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 CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
const val = Number(info.getValue()) || 0
|
||||
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") ||
|
||||
(val < 65 && "bg-green-500") ||
|
||||
(val < 90 && "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])
|
||||
})
|
||||
@@ -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,78 +27,46 @@ 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,
|
||||
parseSemVer,
|
||||
} 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 = Number(info.getValue()) || 0
|
||||
const val = (info.getValue() as number) || 0
|
||||
const userSettings = useStore($userSettings)
|
||||
const yellow = userSettings?.meterThresholds?.yellow ?? 65
|
||||
const red = userSettings?.meterThresholds?.red ?? 90
|
||||
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="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"
|
||||
(val < yellow! && "bg-green-500") ||
|
||||
(val < red! && "bg-yellow-500") ||
|
||||
"bg-red-600"
|
||||
)}
|
||||
style={{
|
||||
transform: `scalex(${val / 100})`,
|
||||
@@ -145,218 +111,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 }) => info.cpu,
|
||||
id: "cpu",
|
||||
name: () => t`CPU`,
|
||||
cell: CellFormatter,
|
||||
Icon: CpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
// accessorKey: "info.mp",
|
||||
accessorFn: ({ info }) => info.mp,
|
||||
id: "memory",
|
||||
name: () => t`Memory`,
|
||||
cell: CellFormatter,
|
||||
Icon: MemoryStickIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.dp,
|
||||
id: "disk",
|
||||
name: () => t`Disk`,
|
||||
cell: CellFormatter,
|
||||
Icon: HardDriveIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: ({ info }) => info.g,
|
||||
id: "gpu",
|
||||
name: () => "GPU",
|
||||
cell: CellFormatter,
|
||||
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
|
||||
}
|
||||
|
||||
function getDotColor() {
|
||||
const normalized = max / (sysInfo.t ?? 1)
|
||||
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: ({ 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">
|
||||
<AlertsButton system={row.original} />
|
||||
<ActionsButton system={row.original} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as ColumnDef<SystemRecord>[]
|
||||
}, [])
|
||||
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
@@ -634,7 +389,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>
|
||||
)}
|
||||
@@ -669,116 +424,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" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ export const $maxValues = atom(false)
|
||||
export const $userSettings = map<UserSettings>({
|
||||
chartTime: "1h",
|
||||
emails: [pb.authStore.record?.email || ""],
|
||||
meterThresholds: {
|
||||
yellow: 65,
|
||||
red: 90,
|
||||
},
|
||||
// unitTemp: "celsius",
|
||||
// unitNet: "mbps",
|
||||
// unitDisk: "mbps",
|
||||
|
||||
@@ -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 "نسخ النص"
|
||||
|
||||
@@ -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 "Копирай текста"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "کپی متن"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "テキストをコピー"
|
||||
|
||||
@@ -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 "텍스트 복사"
|
||||
|
||||
@@ -296,6 +296,10 @@ 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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "Копировать текст"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: sv\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-07-25 22:44\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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "Копіювати текст"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "复制文本"
|
||||
|
||||
@@ -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 "複製文本"
|
||||
|
||||
@@ -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 "複製文字"
|
||||
|
||||
4
beszel/site/src/types.d.ts
vendored
4
beszel/site/src/types.d.ts
vendored
@@ -228,6 +228,10 @@ export interface UserSettings {
|
||||
chartTime: ChartTimes
|
||||
emails?: string[]
|
||||
webhooks?: string[]
|
||||
meterThresholds?: {
|
||||
yellow?: number
|
||||
red?: number
|
||||
}
|
||||
unitTemp?: Unit
|
||||
unitNet?: Unit
|
||||
unitDisk?: Unit
|
||||
|
||||
@@ -3,7 +3,7 @@ package beszel
|
||||
import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
Version = "0.12.1"
|
||||
Version = "0.12.2"
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
373
supplemental/licenses/LibreHardwareMonitor/LICENSE
Normal file
373
supplemental/licenses/LibreHardwareMonitor/LICENSE
Normal 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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user