mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-14 17:01:51 +02:00
Compare commits
30 Commits
custom-col
...
ae7317a272
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae7317a272 | ||
|
|
6140b5c240 | ||
|
|
cd5a04065f | ||
|
|
86576c1b71 | ||
|
|
c5bd07fd46 | ||
|
|
ffb8d06c5a | ||
|
|
eae8f2d51a | ||
|
|
6fa67cc8ef | ||
|
|
ac46cca263 | ||
|
|
81676c7df4 | ||
|
|
8bb6344504 | ||
|
|
4b1f5b21c5 | ||
|
|
550dfd35fa | ||
|
|
d40b488057 | ||
|
|
d80358d770 | ||
|
|
9eaf3ac4b3 | ||
|
|
a7eb1613c3 | ||
|
|
897d8fd6a2 | ||
|
|
2ef3b6f991 | ||
|
|
febf9f3567 | ||
|
|
a23525530b | ||
|
|
ce0d94b9c0 | ||
|
|
0c6a64f994 | ||
|
|
bd7e84544a | ||
|
|
a61a5f3591 | ||
|
|
95cd181e8c | ||
|
|
dc85562e2b | ||
|
|
4acb82a513 | ||
|
|
531d25f2fa | ||
|
|
58c5ffaa39 |
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -3,7 +3,7 @@ name: Make release and binaries
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -29,17 +29,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
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
|
- name: GoReleaser beszel
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
@@ -50,4 +40,3 @@ jobs:
|
|||||||
args: release --clean
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,5 +17,3 @@ beszel/build
|
|||||||
beszel/site/src/locales/**/*.ts
|
beszel/site/src/locales/**/*.ts
|
||||||
*.bak
|
*.bak
|
||||||
__debug_*
|
__debug_*
|
||||||
beszel/internal/agent/lhm/obj
|
|
||||||
beszel/internal/agent/lhm/bin
|
|
||||||
|
|||||||
@@ -202,14 +202,13 @@ winget:
|
|||||||
owner: henrygd
|
owner: henrygd
|
||||||
name: beszel-winget
|
name: beszel-winget
|
||||||
branch: henrygd.beszel-agent-{{ .Version }}
|
branch: henrygd.beszel-agent-{{ .Version }}
|
||||||
token: "{{ .Env.WINGET_TOKEN }}"
|
pull_request:
|
||||||
# pull_request:
|
enabled: true
|
||||||
# enabled: true
|
draft: false
|
||||||
# draft: false
|
base:
|
||||||
# base:
|
owner: microsoft
|
||||||
# owner: microsoft
|
name: winget-pkgs
|
||||||
# name: winget-pkgs
|
branch: master
|
||||||
# branch: master
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
draft: true
|
draft: true
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ ARCH ?= $(shell go env GOARCH)
|
|||||||
# Skip building the web UI if true
|
# Skip building the web UI if true
|
||||||
SKIP_WEB ?= false
|
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
|
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales
|
||||||
.DEFAULT_GOAL := build
|
.DEFAULT_GOAL := build
|
||||||
|
|
||||||
@@ -33,25 +30,11 @@ build-web-ui:
|
|||||||
npm run --prefix ./site build; \
|
npm run --prefix ./site build; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Conditional .NET build - only for Windows
|
build-agent: tidy
|
||||||
build-dotnet-conditional:
|
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/agent
|
||||||
@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)
|
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
||||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
|
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/hub
|
||||||
|
|
||||||
build: build-agent build-hub
|
build: build-agent build-hub
|
||||||
|
|
||||||
@@ -84,15 +67,6 @@ dev-agent:
|
|||||||
else \
|
else \
|
||||||
go run beszel/cmd/agent; \
|
go run beszel/cmd/agent; \
|
||||||
fi
|
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
|
# KEY="..." make -j dev
|
||||||
dev: dev-server dev-hub dev-agent
|
dev: dev-server dev-hub dev-agent
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@@ -26,16 +25,13 @@ func (opts *cmdOptions) parse() bool {
|
|||||||
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
|
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
builder := strings.Builder{}
|
fmt.Printf("Usage: %s [command] [flags]\n", os.Args[0])
|
||||||
builder.WriteString("Usage: ")
|
fmt.Println("\nCommands:")
|
||||||
builder.WriteString(os.Args[0])
|
fmt.Println(" health Check if the agent is running")
|
||||||
builder.WriteString(" [command] [flags]\n")
|
fmt.Println(" help Display this help message")
|
||||||
builder.WriteString("\nCommands:\n")
|
fmt.Println(" update Update to the latest version")
|
||||||
builder.WriteString(" health Check if the agent is running\n")
|
fmt.Println(" version Display the version")
|
||||||
builder.WriteString(" help Display this help message\n")
|
fmt.Println("\nFlags:")
|
||||||
builder.WriteString(" update Update to the latest version\n")
|
|
||||||
builder.WriteString("\nFlags:\n")
|
|
||||||
fmt.Print(builder.String())
|
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,12 +111,12 @@ func main() {
|
|||||||
serverConfig.Addr = addr
|
serverConfig.Addr = addr
|
||||||
serverConfig.Network = agent.GetNetwork(addr)
|
serverConfig.Network = agent.GetNetwork(addr)
|
||||||
|
|
||||||
a, err := agent.NewAgent()
|
agent, err := agent.NewAgent("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to create agent: ", err)
|
log.Fatal("Failed to create agent: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.Start(serverConfig); err != nil {
|
if err := agent.Start(serverConfig); err != nil {
|
||||||
log.Fatal("Failed to start server: ", err)
|
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.
|
// 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.
|
// 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{
|
agent = &Agent{
|
||||||
fsStats: make(map[string]*system.FsStats),
|
fsStats: make(map[string]*system.FsStats),
|
||||||
cache: NewSessionCache(69 * time.Second),
|
cache: NewSessionCache(69 * time.Second),
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.dataDir, err = getDataDir(dataDir...)
|
agent.dataDir, err = getDataDir(dataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Data directory not found")
|
slog.Warn("Data directory not found")
|
||||||
} else {
|
} else {
|
||||||
@@ -113,37 +113,37 @@ func (a *Agent) gatherStats(sessionID string) *system.CombinedData {
|
|||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
data, isCached := a.cache.Get(sessionID)
|
cachedData, ok := a.cache.Get(sessionID)
|
||||||
if isCached {
|
if ok {
|
||||||
slog.Debug("Cached data", "session", sessionID)
|
slog.Debug("Cached stats", "session", sessionID)
|
||||||
return data
|
return cachedData
|
||||||
}
|
}
|
||||||
|
|
||||||
*data = system.CombinedData{
|
*cachedData = system.CombinedData{
|
||||||
Stats: a.getSystemStats(),
|
Stats: a.getSystemStats(),
|
||||||
Info: a.systemInfo,
|
Info: a.systemInfo,
|
||||||
}
|
}
|
||||||
slog.Debug("System data", "data", data)
|
slog.Debug("System stats", "data", cachedData)
|
||||||
|
|
||||||
if a.dockerManager != nil {
|
if a.dockerManager != nil {
|
||||||
if containerStats, err := a.dockerManager.getDockerStats(); err == nil {
|
if containerStats, err := a.dockerManager.getDockerStats(); err == nil {
|
||||||
data.Containers = containerStats
|
cachedData.Containers = containerStats
|
||||||
slog.Debug("Containers", "data", data.Containers)
|
slog.Debug("Docker stats", "data", cachedData.Containers)
|
||||||
} else {
|
} else {
|
||||||
slog.Debug("Containers", "err", err)
|
slog.Debug("Docker stats", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
cachedData.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
for name, stats := range a.fsStats {
|
for name, stats := range a.fsStats {
|
||||||
if !stats.Root && stats.DiskTotal > 0 {
|
if !stats.Root && stats.DiskTotal > 0 {
|
||||||
data.Stats.ExtraFs[name] = stats
|
cachedData.Stats.ExtraFs[name] = stats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
|
slog.Debug("Extra filesystems", "data", cachedData.Stats.ExtraFs)
|
||||||
|
|
||||||
a.cache.Set(sessionID, data)
|
a.cache.Set(sessionID, cachedData)
|
||||||
return data
|
return cachedData
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartAgent initializes and starts the agent with optional WebSocket connection
|
// StartAgent initializes and starts the agent with optional WebSocket connection
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<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
|
// reset high temp
|
||||||
a.systemInfo.DashboardTemp = 0
|
a.systemInfo.DashboardTemp = 0
|
||||||
|
|
||||||
temps, err := a.getTempsWithPanicRecovery(getSensorTemps)
|
temps, err := a.getTempsWithPanicRecovery(sensors.TemperaturesWithContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// retry once on panic (gopsutil/issues/1832)
|
// retry once on panic (gopsutil/issues/1832)
|
||||||
temps, err = a.getTempsWithPanicRecovery(getSensorTemps)
|
temps, err = a.getTempsWithPanicRecovery(sensors.TemperaturesWithContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Error updating temperatures", "err", err)
|
slog.Warn("Error updating temperatures", "err", err)
|
||||||
if len(systemStats.Temperatures) > 0 {
|
if len(systemStats.Temperatures) > 0 {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/shirou/gopsutil/v4/sensors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var getSensorTemps = sensors.TemperaturesWithContext
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
//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,11 +293,18 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
|||||||
// app.Logger().Error("failed to save alert record", "err", err)
|
// app.Logger().Error("failed to save alert record", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
am.SendAlert(AlertMessageData{
|
// expand the user relation and send the alert
|
||||||
UserID: alert.alertRecord.GetString("user"),
|
if errs := am.hub.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||||
Title: subject,
|
// app.Logger().Error("failed to expand user relation", "errs", errs)
|
||||||
Message: body,
|
return
|
||||||
Link: am.hub.MakeLink("system", systemName),
|
}
|
||||||
LinkText: "View " + systemName,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,26 +42,29 @@ func (um *UserManager) InitializeUserRole(e *core.RecordEvent) error {
|
|||||||
// Initialize user settings with defaults if not set
|
// Initialize user settings with defaults if not set
|
||||||
func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
|
func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
|
||||||
record := e.Record
|
record := e.Record
|
||||||
// intialize settings with defaults (zero values can be ignored)
|
// intialize settings with defaults
|
||||||
settings := UserSettings{
|
settings := UserSettings{
|
||||||
ChartTime: "1h",
|
ChartTime: "1h",
|
||||||
|
NotificationEmails: []string{},
|
||||||
|
NotificationWebhooks: []string{},
|
||||||
}
|
}
|
||||||
record.UnmarshalJSONField("settings", &settings)
|
record.UnmarshalJSONField("settings", &settings)
|
||||||
// get user email from auth record
|
if len(settings.NotificationEmails) == 0 {
|
||||||
var user struct {
|
// get user email from auth record
|
||||||
Email string `db:"email"`
|
if errs := um.app.ExpandRecord(record, []string{"user"}, nil); len(errs) == 0 {
|
||||||
}
|
// app.Logger().Error("failed to expand user relation", "errs", errs)
|
||||||
err := e.App.DB().NewQuery("SELECT email FROM users WHERE id = {:id}").Bind(dbx.Params{
|
if user := record.ExpandedOne("user"); user != nil {
|
||||||
"id": record.GetString("user"),
|
settings.NotificationEmails = []string{user.GetString("email")}
|
||||||
}).One(&user)
|
} else {
|
||||||
if err != nil {
|
log.Println("Failed to get user email from auth record")
|
||||||
log.Println("failed to get user email", "err", err)
|
}
|
||||||
return err
|
} else {
|
||||||
}
|
log.Println("failed to expand user relation", "errs", errs)
|
||||||
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)
|
record.Set("settings", settings)
|
||||||
return e.Next()
|
return e.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
4
beszel/site/package-lock.json
generated
4
beszel/site/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"version": "0.12.2",
|
"version": "0.12.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"version": "0.12.2",
|
"version": "0.12.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@henrygd/queue": "^1.0.7",
|
"@henrygd/queue": "^1.0.7",
|
||||||
"@henrygd/semaphore": "^0.0.2",
|
"@henrygd/semaphore": "^0.0.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.12.2",
|
"version": "0.12.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -72,18 +72,18 @@ function AlertDialogContent({ system }: { system: SystemRecord }) {
|
|||||||
const alerts = useStore($alerts)
|
const alerts = useStore($alerts)
|
||||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
||||||
|
|
||||||
/* key to prevent re-rendering */
|
// alertsSignature changes only when alerts for this system change
|
||||||
const alertsSignature: string[] = []
|
let alertsSignature = ""
|
||||||
|
|
||||||
const systemAlerts = alerts.filter((alert) => {
|
const systemAlerts = alerts.filter((alert) => {
|
||||||
if (alert.system === system.id) {
|
if (alert.system === system.id) {
|
||||||
alertsSignature.push(alert.name, alert.min, alert.value)
|
alertsSignature += alert.name + alert.min + alert.value
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}) as AlertRecord[]
|
}) as AlertRecord[]
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
// console.log("render modal", system.name, alertsSignature)
|
||||||
const data = Object.keys(alertInfo).map((name) => {
|
const data = Object.keys(alertInfo).map((name) => {
|
||||||
const alert = alertInfo[name as keyof typeof alertInfo]
|
const alert = alertInfo[name as keyof typeof alertInfo]
|
||||||
return {
|
return {
|
||||||
@@ -149,5 +149,5 @@ function AlertDialogContent({ system }: { system: SystemRecord }) {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [alertsSignature.join(""), overwriteExisting])
|
}, [alertsSignature, overwriteExisting])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,5 +87,5 @@ export default function AreaChartDefault({
|
|||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
|
}, [chartData.systemStats.length, yAxisWidth, maxToggled])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export const Home = memo(() => {
|
|||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
|
|
||||||
/* key to prevent re-rendering of active alerts */
|
let alertsKey = ""
|
||||||
const alertsKey: string[] = []
|
|
||||||
|
|
||||||
const activeAlerts = useMemo(() => {
|
const activeAlerts = useMemo(() => {
|
||||||
const activeAlerts = alerts.filter((alert) => {
|
const activeAlerts = alerts.filter((alert) => {
|
||||||
const active = alert.triggered && alert.name in alertInfo
|
const active = alert.triggered && alert.name in alertInfo
|
||||||
@@ -28,7 +26,7 @@ export const Home = memo(() => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
alert.sysname = systems.find((system) => system.id === alert.system)?.name
|
alert.sysname = systems.find((system) => system.id === alert.system)?.name
|
||||||
alertsKey.push(alert.id)
|
alertsKey += alert.id
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return activeAlerts
|
return activeAlerts
|
||||||
@@ -83,7 +81,7 @@ export const Home = memo(() => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
[alertsKey.join("")]
|
[alertsKey]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,30 +11,17 @@ import { useState } from "react"
|
|||||||
import languages from "@/lib/languages"
|
import languages from "@/lib/languages"
|
||||||
import { dynamicActivate } from "@/lib/i18n"
|
import { dynamicActivate } from "@/lib/i18n"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
// import { setLang } from "@/lib/i18n"
|
|
||||||
import { Unit } from "@/lib/enums"
|
import { Unit } from "@/lib/enums"
|
||||||
|
|
||||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const { i18n } = useLingui()
|
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>) {
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const formData = new FormData(e.target as HTMLFormElement)
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
const data = Object.fromEntries(formData) as Partial<UserSettings>
|
const data = Object.fromEntries(formData) as Partial<UserSettings>
|
||||||
data.meterThresholds = { yellow, red }
|
|
||||||
await saveSettings(data)
|
await saveSettings(data)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
@@ -114,45 +101,6 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<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="space-y-2">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="mb-1 text-lg font-medium">
|
<h3 className="mb-1 text-lg font-medium">
|
||||||
@@ -185,6 +133,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="block" htmlFor="unitNet">
|
<Label className="block" htmlFor="unitNet">
|
||||||
<Trans comment="Context: Bytes or bits">Network unit</Trans>
|
<Trans comment="Context: Bytes or bits">Network unit</Trans>
|
||||||
@@ -207,6 +156,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="block" htmlFor="unitDisk">
|
<Label className="block" htmlFor="unitDisk">
|
||||||
<Trans>Disk unit</Trans>
|
<Trans>Disk unit</Trans>
|
||||||
|
|||||||
@@ -1,420 +0,0 @@
|
|||||||
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,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
CellContext,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
@@ -8,13 +9,14 @@ import {
|
|||||||
VisibilityState,
|
VisibilityState,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
|
HeaderContext,
|
||||||
Row,
|
Row,
|
||||||
Table as TableType,
|
Table as TableType,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -27,46 +29,78 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
|
||||||
import { SystemRecord } from "@/types"
|
import { SystemRecord } from "@/types"
|
||||||
import {
|
import {
|
||||||
|
MoreHorizontalIcon,
|
||||||
ArrowUpDownIcon,
|
ArrowUpDownIcon,
|
||||||
|
MemoryStickIcon,
|
||||||
|
CopyIcon,
|
||||||
|
PauseCircleIcon,
|
||||||
|
PlayCircleIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
WifiIcon,
|
||||||
|
HardDriveIcon,
|
||||||
|
ServerIcon,
|
||||||
|
CpuIcon,
|
||||||
LayoutGridIcon,
|
LayoutGridIcon,
|
||||||
LayoutListIcon,
|
LayoutListIcon,
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
PenBoxIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { memo, useEffect, useMemo, useState } from "react"
|
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { $systems } from "@/lib/stores"
|
import { $systems, $userSettings, pb } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { cn, useLocalStorage } from "@/lib/utils"
|
import {
|
||||||
|
cn,
|
||||||
|
copyToClipboard,
|
||||||
|
isReadOnlyUser,
|
||||||
|
useLocalStorage,
|
||||||
|
formatTemperature,
|
||||||
|
decimalString,
|
||||||
|
formatBytes,
|
||||||
|
parseSemVer,
|
||||||
|
} from "@/lib/utils"
|
||||||
|
import AlertsButton from "../alerts/alert-button"
|
||||||
import { $router, Link, navigate } from "../router"
|
import { $router, Link, navigate } from "../router"
|
||||||
|
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
||||||
import { useLingui, Trans } from "@lingui/react/macro"
|
import { useLingui, Trans } from "@lingui/react/macro"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { Input } from "../ui/input"
|
import { Input } from "../ui/input"
|
||||||
|
import { ClassValue } from "clsx"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
|
import { SystemDialog } from "../add-system"
|
||||||
import AlertButton from "../alerts/alert-button"
|
import { Dialog } from "../ui/dialog"
|
||||||
|
|
||||||
type ViewMode = "table" | "grid"
|
type ViewMode = "table" | "grid"
|
||||||
|
|
||||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||||
const val = (info.getValue() as number) || 0
|
const val = Number(info.getValue()) || 0
|
||||||
const userSettings = useStore($userSettings)
|
|
||||||
const yellow = userSettings?.meterThresholds?.yellow ?? 65
|
|
||||||
const red = userSettings?.meterThresholds?.red ?? 90
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
||||||
<span className="min-w-8">{decimalString(val, 1)}%</span>
|
<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="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 w-full h-full origin-left",
|
"absolute inset-0 w-full h-full origin-left",
|
||||||
(info.row.original.status !== "up" && "bg-primary/30") ||
|
(info.row.original.status !== "up" && "bg-primary/30") ||
|
||||||
(val < yellow! && "bg-green-500") ||
|
(val < 65 && "bg-green-500") ||
|
||||||
(val < red! && "bg-yellow-500") ||
|
(val < 90 && "bg-yellow-500") ||
|
||||||
"bg-red-600"
|
"bg-red-600"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
transform: `scalex(${val / 100})`,
|
transform: `scalex(${val / 100})`,
|
||||||
@@ -111,7 +145,218 @@ export default function SystemsTable() {
|
|||||||
}
|
}
|
||||||
}, [filter])
|
}, [filter])
|
||||||
|
|
||||||
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [])
|
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 table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@@ -389,7 +634,7 @@ const SystemCard = memo(
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
{table.getColumn("actions")?.getIsVisible() && (
|
{table.getColumn("actions")?.getIsVisible() && (
|
||||||
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
||||||
<AlertButton system={system} />
|
<AlertsButton system={system} />
|
||||||
<ActionsButton system={system} />
|
<ActionsButton system={system} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -424,3 +669,116 @@ const SystemCard = memo(
|
|||||||
}, [system, colLength, t])
|
}, [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,10 +28,6 @@ export const $maxValues = atom(false)
|
|||||||
export const $userSettings = map<UserSettings>({
|
export const $userSettings = map<UserSettings>({
|
||||||
chartTime: "1h",
|
chartTime: "1h",
|
||||||
emails: [pb.authStore.record?.email || ""],
|
emails: [pb.authStore.record?.email || ""],
|
||||||
meterThresholds: {
|
|
||||||
yellow: 65,
|
|
||||||
red: 90,
|
|
||||||
},
|
|
||||||
// unitTemp: "celsius",
|
// unitTemp: "celsius",
|
||||||
// unitNet: "mbps",
|
// unitNet: "mbps",
|
||||||
// unitDisk: "mbps",
|
// unitDisk: "mbps",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ar\n"
|
"Language: ar\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-07-25 22:44\n"
|
"PO-Revision-Date: 2025-07-25 23:46\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Arabic\n"
|
"Language-Team: Arabic\n"
|
||||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||||
@@ -296,10 +296,6 @@ msgstr "نسخ المضيف"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "نسخ أمر لينكس"
|
msgstr "نسخ أمر لينكس"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "نسخ الاسم"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "نسخ النص"
|
msgstr "نسخ النص"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "تكوين YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Копирай хоста"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Копирай linux командата"
|
msgstr "Копирай linux командата"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Копирай име"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Копирай текста"
|
msgstr "Копирай текста"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML конфигурация"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Настройките за потребителя ти са обновени."
|
msgstr "Настройките за потребителя ти са обновени."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopírovat hostitele"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopírovat příkaz Linux"
|
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
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopírovat text"
|
msgstr "Kopírovat text"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML konfigurace"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vaše uživatelská nastavení byla aktualizována."
|
msgstr "Vaše uživatelská nastavení byla aktualizována."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopier host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopier Linux kommando"
|
msgstr "Kopier Linux kommando"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopier navn"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopier tekst"
|
msgstr "Kopier tekst"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML Konfiguration"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Dine brugerindstillinger er opdateret."
|
msgstr "Dine brugerindstillinger er opdateret."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Host kopieren"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Linux-Befehl kopieren"
|
msgstr "Linux-Befehl kopieren"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Name kopieren"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Text kopieren"
|
msgstr "Text kopieren"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML-Konfiguration"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
||||||
|
|
||||||
|
|||||||
1114
beszel/site/src/locales/el/el.po
Normal file
1114
beszel/site/src/locales/el/el.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -291,10 +291,6 @@ msgstr "Copy host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "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
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Copy text"
|
msgstr "Copy text"
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Copiar host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Copiar comando de Linux"
|
msgstr "Copiar comando de Linux"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Copiar nombre"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Copiar texto"
|
msgstr "Copiar texto"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Configuración YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Su configuración de usuario ha sido actualizada."
|
msgstr "Su configuración de usuario ha sido actualizada."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "کپی میزبان"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "کپی دستور لینوکس"
|
msgstr "کپی دستور لینوکس"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "کپی نام"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "کپی متن"
|
msgstr "کپی متن"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "پیکربندی YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-07-25 22:44\n"
|
"PO-Revision-Date: 2025-07-25 23:46\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: French\n"
|
"Language-Team: French\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
@@ -296,10 +296,6 @@ msgstr "Copier l'hôte"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Copier la commande Linux"
|
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
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Copier le texte"
|
msgstr "Copier le texte"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Configuration YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vos paramètres utilisateur ont été mis à jour."
|
msgstr "Vos paramètres utilisateur ont été mis à jour."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopiraj hosta"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopiraj Linux komandu"
|
msgstr "Kopiraj Linux komandu"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopiraj naziv"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopiraj tekst"
|
msgstr "Kopiraj tekst"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML Konfiguracija"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vaše korisničke postavke su ažurirane."
|
msgstr "Vaše korisničke postavke su ažurirane."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Hoszt másolása"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Linux parancs másolása"
|
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
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Szöveg másolása"
|
msgstr "Szöveg másolása"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML konfiguráció"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "A felhasználói beállítások frissítésre kerültek."
|
msgstr "A felhasználói beállítások frissítésre kerültek."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Afrita host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Afrita Linux aðgerð"
|
msgstr "Afrita Linux aðgerð"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Afrita nafn"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Afrita texta"
|
msgstr "Afrita texta"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr ""
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Notenda stillingar vistaðar."
|
msgstr "Notenda stillingar vistaðar."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Copia host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Copia comando Linux"
|
msgstr "Copia comando Linux"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Copia nome"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Copia testo"
|
msgstr "Copia testo"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Configurazione YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Le impostazioni utente sono state aggiornate."
|
msgstr "Le impostazioni utente sono state aggiornate."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "ホストをコピー"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Linuxコマンドをコピー"
|
msgstr "Linuxコマンドをコピー"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "名前をコピー"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "テキストをコピー"
|
msgstr "テキストをコピー"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML設定"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "ユーザー設定が更新されました。"
|
msgstr "ユーザー設定が更新されました。"
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "호스트 복사"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "리눅스 명령어 복사"
|
msgstr "리눅스 명령어 복사"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "이름 복사"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "텍스트 복사"
|
msgstr "텍스트 복사"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML 구성"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "사용자 설정이 업데이트되었습니다."
|
msgstr "사용자 설정이 업데이트되었습니다."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopieer host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopieer Linux-opdracht"
|
msgstr "Kopieer Linux-opdracht"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopieer naam"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopieer tekst"
|
msgstr "Kopieer tekst"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML Configuratie"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopier vert"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopier Linux-kommando"
|
msgstr "Kopier Linux-kommando"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopier navn"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopier tekst"
|
msgstr "Kopier tekst"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML Konfigurasjon"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopiuj host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopiuj polecenie Linux"
|
msgstr "Kopiuj polecenie Linux"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopiuj nazwę"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopiuj tekst"
|
msgstr "Kopiuj tekst"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Konfiguracja YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Copiar host"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Copiar comando Linux"
|
msgstr "Copiar comando Linux"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Copiar nome"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Copiar texto"
|
msgstr "Copiar texto"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Configuração YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "As configurações do seu usuário foram atualizadas."
|
msgstr "As configurações do seu usuário foram atualizadas."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Копировать хост"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Копировать команду Linux"
|
msgstr "Копировать команду Linux"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Копировать имя"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Копировать текст"
|
msgstr "Копировать текст"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML конфигурация"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Ваши настройки пользователя были обновлены."
|
msgstr "Ваши настройки пользователя были обновлены."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Kopiraj gostitelja"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopiraj Linux ukaz"
|
msgstr "Kopiraj Linux ukaz"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopiraj ime"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopiraj besedilo"
|
msgstr "Kopiraj besedilo"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML nastavitev"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: sv\n"
|
"Language: sv\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-08-01 23:21\n"
|
"PO-Revision-Date: 2025-07-25 22:44\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Swedish\n"
|
"Language-Team: Swedish\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\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
|
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "{0} of {1} row(s) selected."
|
msgid "{0} of {1} row(s) selected."
|
||||||
msgstr "{0} av {1} rad(er) valda."
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
msgid "{hours, plural, one {# hour} other {# hours}}"
|
msgid "{hours, plural, one {# hour} other {# hours}}"
|
||||||
@@ -40,7 +40,7 @@ msgstr "1 timme"
|
|||||||
#. Load average
|
#. Load average
|
||||||
#: src/components/charts/load-average-chart.tsx
|
#: src/components/charts/load-average-chart.tsx
|
||||||
msgid "1 min"
|
msgid "1 min"
|
||||||
msgstr "1 min"
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 week"
|
msgid "1 week"
|
||||||
@@ -53,7 +53,7 @@ msgstr "12 timmar"
|
|||||||
#. Load average
|
#. Load average
|
||||||
#: src/components/charts/load-average-chart.tsx
|
#: src/components/charts/load-average-chart.tsx
|
||||||
msgid "15 min"
|
msgid "15 min"
|
||||||
msgstr "15 min"
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "24 hours"
|
msgid "24 hours"
|
||||||
@@ -66,7 +66,7 @@ msgstr "30 dagar"
|
|||||||
#. Load average
|
#. Load average
|
||||||
#: src/components/charts/load-average-chart.tsx
|
#: src/components/charts/load-average-chart.tsx
|
||||||
msgid "5 min"
|
msgid "5 min"
|
||||||
msgstr "5 min"
|
msgstr ""
|
||||||
|
|
||||||
#. Table column
|
#. Table column
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
@@ -77,7 +77,7 @@ msgstr "Åtgärder"
|
|||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Aktiv"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/home.tsx
|
#: src/components/routes/home.tsx
|
||||||
msgid "Active Alerts"
|
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
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr "Är du säker?"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Automatic copy requires a secure context."
|
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
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||||
msgstr "Bits (Kbps, Mbps, Gbps)"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||||
msgstr "Bytes (KB/s, MB/s, GB/S)"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/charts/mem-chart.tsx
|
#: src/components/charts/mem-chart.tsx
|
||||||
msgid "Cache / Buffers"
|
msgid "Cache / Buffers"
|
||||||
@@ -213,11 +213,11 @@ msgstr "Varning - potentiell dataförlust"
|
|||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Celsius (°C)"
|
msgid "Celsius (°C)"
|
||||||
msgstr "Celsius (°C)"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
msgstr "Ändra enheter för mätvärden."
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change general application options."
|
msgid "Change general application options."
|
||||||
@@ -259,7 +259,7 @@ msgstr "Bekräfta lösenord"
|
|||||||
|
|
||||||
#: src/components/routes/home.tsx
|
#: src/components/routes/home.tsx
|
||||||
msgid "Connection is down"
|
msgid "Connection is down"
|
||||||
msgstr "Ej ansluten"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
@@ -296,10 +296,6 @@ msgstr "Kopiera värd"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Kopiera Linux-kommando"
|
msgstr "Kopiera Linux-kommando"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Kopiera namn"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Kopiera text"
|
msgstr "Kopiera text"
|
||||||
@@ -314,7 +310,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Copy YAML"
|
msgid "Copy YAML"
|
||||||
msgstr "Kopiera YAML"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "CPU"
|
msgid "CPU"
|
||||||
@@ -333,7 +329,7 @@ msgstr "Skapa konto"
|
|||||||
#. Context: date created
|
#. Context: date created
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
msgid "Created"
|
msgid "Created"
|
||||||
msgstr "Skapad"
|
msgstr ""
|
||||||
|
|
||||||
#. Dark theme
|
#. Dark theme
|
||||||
#: src/components/mode-toggle.tsx
|
#: src/components/mode-toggle.tsx
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML-konfiguration"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Dina användarinställningar har uppdaterats."
|
msgstr "Dina användarinställningar har uppdaterats."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Ana bilgisayarı kopyala"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Linux komutunu kopyala"
|
msgstr "Linux komutunu kopyala"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Adı kopyala"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Metni kopyala"
|
msgstr "Metni kopyala"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML Yapılandırması"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Kullanıcı ayarlarınız güncellendi."
|
msgstr "Kullanıcı ayarlarınız güncellendi."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Копіювати хост"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Копіювати команду Linux"
|
msgstr "Копіювати команду Linux"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "Копіювати імʼя"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Копіювати текст"
|
msgstr "Копіювати текст"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Конфігурація YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Ваші налаштування користувача були оновлені."
|
msgstr "Ваші налаштування користувача були оновлені."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "Sao chép máy chủ"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "Sao chép lệnh Linux"
|
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
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "Sao chép văn bản"
|
msgstr "Sao chép văn bản"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "Cấu hình YAML"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Cài đặt người dùng của bạn đã được cập nhật."
|
msgstr "Cài đặt người dùng của bạn đã được cập nhật."
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "复制主机名"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "复制 Linux 安装命令"
|
msgstr "复制 Linux 安装命令"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "复制名称"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "复制文本"
|
msgstr "复制文本"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML配置"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "您的用户设置已更新。"
|
msgstr "您的用户设置已更新。"
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "複製主機"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "複製 Linux 指令"
|
msgstr "複製 Linux 指令"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "複製名稱"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "複製文本"
|
msgstr "複製文本"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML配置"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "您的用戶設置已更新。"
|
msgstr "您的用戶設置已更新。"
|
||||||
|
|
||||||
|
|||||||
@@ -296,10 +296,6 @@ msgstr "複製主機"
|
|||||||
msgid "Copy Linux command"
|
msgid "Copy Linux command"
|
||||||
msgstr "複製 Linux 指令"
|
msgstr "複製 Linux 指令"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table.tsx
|
|
||||||
msgid "Copy name"
|
|
||||||
msgstr "複製名稱"
|
|
||||||
|
|
||||||
#: src/components/copy-to-clipboard.tsx
|
#: src/components/copy-to-clipboard.tsx
|
||||||
msgid "Copy text"
|
msgid "Copy text"
|
||||||
msgstr "複製文字"
|
msgstr "複製文字"
|
||||||
@@ -1115,3 +1111,4 @@ msgstr "YAML 設定檔"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "已更新您的使用者設定"
|
msgstr "已更新您的使用者設定"
|
||||||
|
|
||||||
|
|||||||
4
beszel/site/src/types.d.ts
vendored
4
beszel/site/src/types.d.ts
vendored
@@ -228,10 +228,6 @@ export interface UserSettings {
|
|||||||
chartTime: ChartTimes
|
chartTime: ChartTimes
|
||||||
emails?: string[]
|
emails?: string[]
|
||||||
webhooks?: string[]
|
webhooks?: string[]
|
||||||
meterThresholds?: {
|
|
||||||
yellow?: number
|
|
||||||
red?: number
|
|
||||||
}
|
|
||||||
unitTemp?: Unit
|
unitTemp?: Unit
|
||||||
unitNet?: Unit
|
unitNet?: Unit
|
||||||
unitDisk?: Unit
|
unitDisk?: Unit
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package beszel
|
|||||||
import "github.com/blang/semver"
|
import "github.com/blang/semver"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "0.12.2"
|
Version = "0.12.0"
|
||||||
AppName = "beszel"
|
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.
|
- **Lightweight**: Smaller and less resource-intensive than leading solutions.
|
||||||
- **Simple**: Easy setup with little manual configuration required.
|
- **Simple**: Easy setup with little manual configuration required.
|
||||||
- **Docker stats**: Tracks CPU, memory, and network usage history for each container.
|
- **Docker stats**: Tracks CPU, memory, and network usage history for each container.
|
||||||
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, load average, and status.
|
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, and status.
|
||||||
- **Multi-user**: Users manage their own systems. Admins can share systems across users.
|
- **Multi-user**: Users manage their own systems. Admins can share systems across users.
|
||||||
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
||||||
- **Automatic backups**: Save to and restore from disk or S3-compatible storage.
|
- **Automatic backups**: Save to and restore from disk or S3-compatible storage.
|
||||||
|
|||||||
@@ -31,12 +31,10 @@ if ! getent passwd "$SERVICE_USER" >/dev/null; then
|
|||||||
--gecos "System user for $SERVICE"
|
--gecos "System user for $SERVICE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Enable docker (only if docker group exists)
|
# Enable docker
|
||||||
if getent group docker >/dev/null 2>&1; then
|
if ! getent group docker | grep -q "$SERVICE_USER"; then
|
||||||
if ! getent group docker | grep -q "$SERVICE_USER"; then
|
echo "Adding $SERVICE_USER to docker group"
|
||||||
echo "Adding $SERVICE_USER to docker group"
|
usermod -aG docker "$SERVICE_USER"
|
||||||
usermod -aG docker "$SERVICE_USER"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create config file if it doesn't already exist
|
# Create config file if it doesn't already exist
|
||||||
|
|||||||
@@ -1,373 +0,0 @@
|
|||||||
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() {
|
is_openwrt() {
|
||||||
grep -qi "OpenWrt" /etc/os-release
|
cat /etc/os-release | grep -q "OpenWrt"
|
||||||
}
|
}
|
||||||
|
|
||||||
# If SELinux is enabled, set the context of the binary
|
# 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
|
rm -f /var/log/beszel-agent.log /var/log/beszel-agent.err
|
||||||
elif is_openwrt; then
|
elif is_openwrt; then
|
||||||
echo "Stopping and disabling the agent service..."
|
echo "Stopping and disabling the agent service..."
|
||||||
/etc/init.d/beszel-agent stop
|
service beszel-agent stop
|
||||||
/etc/init.d/beszel-agent disable
|
service beszel-agent disable
|
||||||
|
|
||||||
echo "Removing the OpenWRT service files..."
|
echo "Removing the OpenWRT service files..."
|
||||||
rm -f /etc/init.d/beszel-agent
|
rm -f /etc/init.d/beszel-agent
|
||||||
@@ -288,13 +288,13 @@ package_installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Check for package manager and install necessary packages if not installed
|
# Check for package manager and install necessary packages if not installed
|
||||||
if package_installed apk; then
|
if is_alpine; then
|
||||||
if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then
|
if ! package_installed tar || ! package_installed curl || ! package_installed coreutils; then
|
||||||
apk update
|
apk update
|
||||||
apk add tar curl coreutils shadow
|
apk add tar curl coreutils shadow
|
||||||
fi
|
fi
|
||||||
elif package_installed opkg; then
|
elif is_openwrt; then
|
||||||
if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then
|
if ! package_installed tar || ! package_installed curl || ! package_installed coreutils; then
|
||||||
opkg update
|
opkg update
|
||||||
opkg install tar curl coreutils
|
opkg install tar curl coreutils
|
||||||
fi
|
fi
|
||||||
@@ -335,10 +335,11 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create a dedicated user for the service if it doesn't exist
|
# 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 is_alpine; then
|
||||||
if ! id -u beszel >/dev/null 2>&1; then
|
if ! id -u beszel >/dev/null 2>&1; then
|
||||||
|
echo "Creating a dedicated group for the Beszel Agent service..."
|
||||||
addgroup beszel
|
addgroup beszel
|
||||||
|
echo "Creating a dedicated user for the Beszel Agent service..."
|
||||||
adduser -S -D -H -s /sbin/nologin -G beszel beszel
|
adduser -S -D -H -s /sbin/nologin -G beszel beszel
|
||||||
fi
|
fi
|
||||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||||
@@ -346,37 +347,10 @@ if is_alpine; then
|
|||||||
echo "Adding beszel to docker group"
|
echo "Adding beszel to docker group"
|
||||||
usermod -aG docker beszel
|
usermod -aG docker beszel
|
||||||
fi
|
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
|
else
|
||||||
if ! id -u beszel >/dev/null 2>&1; then
|
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
|
useradd --system --home-dir /nonexistent --shell /bin/false beszel
|
||||||
fi
|
fi
|
||||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||||
@@ -453,11 +427,6 @@ set_selinux_context
|
|||||||
# Cleanup
|
# Cleanup
|
||||||
rm -rf "$TEMP_DIR"
|
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
|
# Check for NVIDIA GPUs and grant device permissions for systemd service
|
||||||
detect_nvidia_devices() {
|
detect_nvidia_devices() {
|
||||||
local devices=""
|
local devices=""
|
||||||
@@ -577,7 +546,10 @@ start_service() {
|
|||||||
procd_set_param command /opt/beszel-agent/beszel-agent
|
procd_set_param command /opt/beszel-agent/beszel-agent
|
||||||
procd_set_param user beszel
|
procd_set_param user beszel
|
||||||
procd_set_param pidfile /var/run/beszel-agent.pid
|
procd_set_param pidfile /var/run/beszel-agent.pid
|
||||||
procd_set_param env PORT="$PORT" KEY="$KEY" TOKEN="$TOKEN" HUB_URL="$HUB_URL"
|
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 stdout 1
|
procd_set_param stdout 1
|
||||||
procd_set_param stderr 1
|
procd_set_param stderr 1
|
||||||
procd_close_instance
|
procd_close_instance
|
||||||
@@ -601,10 +573,10 @@ EOF
|
|||||||
|
|
||||||
# Enable the service
|
# Enable the service
|
||||||
chmod +x /etc/init.d/beszel-agent
|
chmod +x /etc/init.d/beszel-agent
|
||||||
/etc/init.d/beszel-agent enable
|
service beszel-agent enable
|
||||||
|
|
||||||
# Start the service
|
# Start the service
|
||||||
/etc/init.d/beszel-agent restart
|
service beszel-agent restart
|
||||||
|
|
||||||
# Auto-update service for OpenWRT using a crontab job
|
# Auto-update service for OpenWRT using a crontab job
|
||||||
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
|
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
|
||||||
@@ -632,9 +604,9 @@ EOF
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# Check service status
|
# Check service status
|
||||||
if ! /etc/init.d/beszel-agent running >/dev/null 2>&1; then
|
if ! service beszel-agent running >/dev/null 2>&1; then
|
||||||
echo "Error: The Beszel Agent service is not running."
|
echo "Error: The Beszel Agent service is not running."
|
||||||
/etc/init.d/beszel-agent status
|
service beszel-agent status
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,13 @@ function Update-ServicePath {
|
|||||||
throw "NSSM is not available in PATH and no valid NSSMPath was provided"
|
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
|
# Update the application path
|
||||||
& $nssmCommand set beszel-agent Application $NewAgentPath
|
& $nssmCommand set beszel-agent Application $NewAgentPath
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
@@ -226,25 +233,7 @@ function Update-ServicePath {
|
|||||||
Write-Host "Service path updated to: $NewAgentPath"
|
Write-Host "Service path updated to: $NewAgentPath"
|
||||||
|
|
||||||
# Start the service
|
# 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..."
|
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
|
& $nssmCommand start beszel-agent
|
||||||
$startResult = $LASTEXITCODE
|
$startResult = $LASTEXITCODE
|
||||||
|
|
||||||
@@ -266,7 +255,7 @@ function Start-BeszelAgentService {
|
|||||||
|
|
||||||
if ($serviceStatus -eq "SERVICE_RUNNING") {
|
if ($serviceStatus -eq "SERVICE_RUNNING") {
|
||||||
$serviceStarted = $true
|
$serviceStarted = $true
|
||||||
Write-Host "Success! The beszel-agent service is now running." -ForegroundColor Green
|
Write-Host "Success! The beszel-agent service is now running with the updated path." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
elseif ($serviceStatus -like "*PENDING*") {
|
elseif ($serviceStatus -like "*PENDING*") {
|
||||||
Write-Host "Service is still starting (status: $serviceStatus)... waiting" -ForegroundColor Yellow
|
Write-Host "Service is still starting (status: $serviceStatus)... waiting" -ForegroundColor Yellow
|
||||||
@@ -284,7 +273,7 @@ function Start-BeszelAgentService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
# NSSM start command was successful
|
# NSSM start command was successful
|
||||||
Write-Host "Success! The beszel-agent service is running properly." -ForegroundColor Green
|
Write-Host "Success! The beszel-agent service is running with the updated path." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +287,7 @@ $isAdmin = Test-Admin
|
|||||||
try {
|
try {
|
||||||
Write-Host "Beszel Agent Upgrade Script" -ForegroundColor Cyan
|
Write-Host "Beszel Agent Upgrade Script" -ForegroundColor Cyan
|
||||||
Write-Host "===========================" -ForegroundColor Cyan
|
Write-Host "===========================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
# First: Check if service exists (doesn't require admin)
|
# First: Check if service exists (doesn't require admin)
|
||||||
$existingService = Get-Service -Name "beszel-agent" -ErrorAction SilentlyContinue
|
$existingService = Get-Service -Name "beszel-agent" -ErrorAction SilentlyContinue
|
||||||
@@ -322,13 +312,6 @@ try {
|
|||||||
Write-Host "Retrieving current service configuration..."
|
Write-Host "Retrieving current service configuration..."
|
||||||
$currentConfig = Get-ServiceConfiguration -NSSMPath $nssmPath
|
$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)
|
# Upgrade the agent (doesn't require admin)
|
||||||
Write-Host "Upgrading beszel-agent..."
|
Write-Host "Upgrading beszel-agent..."
|
||||||
$newAgentPath = $null
|
$newAgentPath = $null
|
||||||
@@ -357,8 +340,7 @@ try {
|
|||||||
|
|
||||||
# Check if the path has changed
|
# Check if the path has changed
|
||||||
if ($currentConfig.CurrentPath -eq $newAgentPath) {
|
if ($currentConfig.CurrentPath -eq $newAgentPath) {
|
||||||
Write-Host "Agent path has not changed. Restarting service..." -ForegroundColor Green
|
Write-Host "Agent path has not changed. Service update not needed." -ForegroundColor Green
|
||||||
Start-BeszelAgentService -NSSMPath $nssmPath
|
|
||||||
Write-Host "Upgrade completed successfully!" -ForegroundColor Green
|
Write-Host "Upgrade completed successfully!" -ForegroundColor Green
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user