mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-18 08:17:47 +02:00
Compare commits
8 Commits
e5507fa106
...
smart-devi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
214180a431 | ||
|
|
d3a1d61955 | ||
|
|
9708e24fd2 | ||
|
|
17e910a246 | ||
|
|
0e65a2373f | ||
|
|
c1c1cd1bcb | ||
|
|
cd9ea51039 | ||
|
|
a71617e058 |
@@ -4,11 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/agent/health"
|
"github.com/henrygd/beszel/agent/health"
|
||||||
|
"github.com/henrygd/beszel/agent/utils"
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,13 +115,36 @@ func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
|
|||||||
_ = health.Update()
|
_ = health.Update()
|
||||||
case <-sigCtx.Done():
|
case <-sigCtx.Done():
|
||||||
slog.Info("Shutting down", "cause", context.Cause(sigCtx))
|
slog.Info("Shutting down", "cause", context.Cause(sigCtx))
|
||||||
_ = c.agent.StopServer()
|
return c.stop()
|
||||||
c.closeWebSocket()
|
|
||||||
return health.CleanUp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop does not stop the connection manager itself, just any active connections. The manager will attempt to reconnect after stopping, so this should only be called immediately before shutting down the entire agent.
|
||||||
|
//
|
||||||
|
// If we need or want to expose a graceful Stop method in the future, do something like this to actually stop the manager:
|
||||||
|
//
|
||||||
|
// func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
|
||||||
|
// ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
// c.cancel = cancel
|
||||||
|
//
|
||||||
|
// for {
|
||||||
|
// select {
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return c.stop()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (c *ConnectionManager) Stop() {
|
||||||
|
// c.cancel()
|
||||||
|
// }
|
||||||
|
func (c *ConnectionManager) stop() error {
|
||||||
|
_ = c.agent.StopServer()
|
||||||
|
c.closeWebSocket()
|
||||||
|
return health.CleanUp()
|
||||||
|
}
|
||||||
|
|
||||||
// handleEvent processes connection events and updates the connection state accordingly.
|
// handleEvent processes connection events and updates the connection state accordingly.
|
||||||
func (c *ConnectionManager) handleEvent(event ConnectionEvent) {
|
func (c *ConnectionManager) handleEvent(event ConnectionEvent) {
|
||||||
switch event {
|
switch event {
|
||||||
@@ -185,9 +212,16 @@ func (c *ConnectionManager) connect() {
|
|||||||
|
|
||||||
// Try WebSocket first, if it fails, start SSH server
|
// Try WebSocket first, if it fails, start SSH server
|
||||||
err := c.startWebSocketConnection()
|
err := c.startWebSocketConnection()
|
||||||
if err != nil && c.State == Disconnected {
|
if err != nil {
|
||||||
c.startSSHServer()
|
if shouldExitOnErr(err) {
|
||||||
c.startWsTicker()
|
time.Sleep(2 * time.Second) // prevent tight restart loop
|
||||||
|
_ = c.stop()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if c.State == Disconnected {
|
||||||
|
c.startSSHServer()
|
||||||
|
c.startWsTicker()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,3 +258,14 @@ func (c *ConnectionManager) closeWebSocket() {
|
|||||||
c.wsClient.Close()
|
c.wsClient.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldExitOnErr checks if the error is a DNS resolution failure and if the
|
||||||
|
// EXIT_ON_DNS_ERROR env var is set. https://github.com/henrygd/beszel/issues/1924.
|
||||||
|
func shouldExitOnErr(err error) bool {
|
||||||
|
if val, _ := utils.GetEnv("EXIT_ON_DNS_ERROR"); val == "true" {
|
||||||
|
if opErr, ok := errors.AsType[*net.OpError](err); ok {
|
||||||
|
return strings.Contains(opErr.Err.Error(), "lookup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -298,3 +299,65 @@ func TestConnectionManager_ConnectFlow(t *testing.T) {
|
|||||||
cm.connect()
|
cm.connect()
|
||||||
}, "Connect should not panic without WebSocket client")
|
}, "Connect should not panic without WebSocket client")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldExitOnErr(t *testing.T) {
|
||||||
|
createDialErr := func(msg string) error {
|
||||||
|
return &net.OpError{
|
||||||
|
Op: "dial",
|
||||||
|
Net: "tcp",
|
||||||
|
Err: errors.New(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
envValue string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no env var",
|
||||||
|
err: createDialErr("lookup lkahsdfasdf: no such host"),
|
||||||
|
envValue: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "env var false",
|
||||||
|
err: createDialErr("lookup lkahsdfasdf: no such host"),
|
||||||
|
envValue: "false",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "env var true, matching error",
|
||||||
|
err: createDialErr("lookup lkahsdfasdf: no such host"),
|
||||||
|
envValue: "true",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "env var true, matching error with extra context",
|
||||||
|
err: createDialErr("lookup beszel.server.lan on [::1]:53: read udp [::1]:44557->[::1]:53: read: connection refused"),
|
||||||
|
envValue: "true",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "env var true, non-matching error",
|
||||||
|
err: errors.New("connection refused"),
|
||||||
|
envValue: "true",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "env var true, dial but not lookup",
|
||||||
|
err: createDialErr("connection timeout"),
|
||||||
|
envValue: "true",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Setenv("EXIT_ON_DNS_ERROR", tt.envValue)
|
||||||
|
result := shouldExitOnErr(tt.err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ type deviceKey struct {
|
|||||||
|
|
||||||
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
||||||
|
|
||||||
|
// isBridgePassthroughType reports whether the device type uses a USB bridge
|
||||||
|
// passthrough driver that may fail transiently (exit 2) due to wakeup data
|
||||||
|
// or other bridge-specific quirks. A single retry is attempted in CollectSmart.
|
||||||
|
func isBridgePassthroughType(deviceType string) bool {
|
||||||
|
dt := strings.ToLower(deviceType)
|
||||||
|
return strings.HasPrefix(dt, "jms56x") || strings.HasPrefix(dt, "jmb39x")
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh updates SMART data for all known devices
|
// Refresh updates SMART data for all known devices
|
||||||
func (sm *SmartManager) Refresh(forceScan bool) error {
|
func (sm *SmartManager) Refresh(forceScan bool) error {
|
||||||
sm.refreshMutex.Lock()
|
sm.refreshMutex.Lock()
|
||||||
@@ -368,9 +376,15 @@ func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte)
|
|||||||
Type string
|
Type string
|
||||||
Parse func([]byte) (bool, int)
|
Parse func([]byte) (bool, int)
|
||||||
}{
|
}{
|
||||||
{Type: "nvme", Parse: sm.parseSmartForNvme},
|
{Type: "nvme", Parse: func(output []byte) (bool, int) {
|
||||||
{Type: "sat", Parse: sm.parseSmartForSata},
|
return sm.parseSmartForNvme(output, deviceInfo.Type)
|
||||||
{Type: "scsi", Parse: sm.parseSmartForScsi},
|
}},
|
||||||
|
{Type: "sat", Parse: func(output []byte) (bool, int) {
|
||||||
|
return sm.parseSmartForSata(output, deviceInfo.Type)
|
||||||
|
}},
|
||||||
|
{Type: "scsi", Parse: func(output []byte) (bool, int) {
|
||||||
|
return sm.parseSmartForScsi(output, deviceInfo.Type)
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceType := normalizeParserType(deviceInfo.parserType)
|
deviceType := normalizeParserType(deviceInfo.parserType)
|
||||||
@@ -495,15 +509,22 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
|||||||
// Check if device is in standby (exit status 2)
|
// Check if device is in standby (exit status 2)
|
||||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 2 {
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 2 {
|
||||||
if hasExistingData {
|
if hasExistingData {
|
||||||
// Device is in standby and we have cached data, keep using cache
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// No cached data, need to collect initial data by bypassing standby
|
// No cached data, retry without -n standby
|
||||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer cancel2()
|
|
||||||
args = sm.smartctlArgs(deviceInfo, false)
|
args = sm.smartctlArgs(deviceInfo, false)
|
||||||
cmd = exec.CommandContext(ctx2, sm.smartctlPath, args...)
|
cmd = exec.CommandContext(ctx, sm.smartctlPath, args...)
|
||||||
output, err = cmd.CombinedOutput()
|
output, err = cmd.CombinedOutput()
|
||||||
|
|
||||||
|
// Bridge passthrough drivers (jms56x, jmb39x) can fail siently due
|
||||||
|
// to wakeup data left by the bridge firmware. A second retry succeeds
|
||||||
|
// after the first attempt has cleared the bridge state.
|
||||||
|
if deviceInfo != nil && isBridgePassthroughType(deviceInfo.Type) {
|
||||||
|
if exitErr2, ok2 := errors.AsType[*exec.ExitError](err); ok2 && exitErr2.ExitCode() == 2 {
|
||||||
|
cmd = exec.CommandContext(ctx, sm.smartctlPath, args...)
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidData := sm.parseSmartOutput(deviceInfo, output)
|
hasValidData := sm.parseSmartOutput(deviceInfo, output)
|
||||||
@@ -733,6 +754,7 @@ func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo
|
|||||||
}
|
}
|
||||||
if existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {
|
if existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {
|
||||||
applyConfiguredMetadata(existingDev, configuredDevice)
|
applyConfiguredMetadata(existingDev, configuredDevice)
|
||||||
|
delete(deviceIndexByName, configuredDevice.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,9 +858,12 @@ func (sm *SmartManager) isVirtualDeviceFromStrings(fields ...string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap
|
// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap.
|
||||||
// Returns hasValidData and exitStatus
|
// configuredType is the user-specified device type (e.g., "sat", "jms56x,0") from SMART_DEVICES.
|
||||||
func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
// When non-empty, it overrides the disk type reported by smartctl in SmartData.DiskType so that
|
||||||
|
// updateSmartDevices can match entries by the configured composite (name, type) key.
|
||||||
|
// Returns hasValidData and exitStatus.
|
||||||
|
func (sm *SmartManager) parseSmartForSata(output []byte, configuredType string) (bool, int) {
|
||||||
var data smart.SmartInfoForSata
|
var data smart.SmartInfoForSata
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
if err := json.Unmarshal(output, &data); err != nil {
|
||||||
@@ -877,6 +902,9 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
|||||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||||
smartData.DiskName = data.Device.Name
|
smartData.DiskName = data.Device.Name
|
||||||
smartData.DiskType = data.Device.Type
|
smartData.DiskType = data.Device.Type
|
||||||
|
if configuredType != "" {
|
||||||
|
smartData.DiskType = normalizeParserType(configuredType)
|
||||||
|
}
|
||||||
|
|
||||||
// get values from ata_device_statistics if necessary
|
// get values from ata_device_statistics if necessary
|
||||||
var ataDeviceStats smart.AtaDeviceStatistics
|
var ataDeviceStats smart.AtaDeviceStatistics
|
||||||
@@ -950,7 +978,7 @@ func findAtaDeviceStatisticsValue(data *smart.SmartInfoForSata, ataDeviceStats *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
func (sm *SmartManager) parseSmartForScsi(output []byte, configuredType string) (bool, int) {
|
||||||
var data smart.SmartInfoForScsi
|
var data smart.SmartInfoForScsi
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
if err := json.Unmarshal(output, &data); err != nil {
|
||||||
@@ -985,6 +1013,9 @@ func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
|||||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||||
smartData.DiskName = data.Device.Name
|
smartData.DiskName = data.Device.Name
|
||||||
smartData.DiskType = data.Device.Type
|
smartData.DiskType = data.Device.Type
|
||||||
|
if configuredType != "" {
|
||||||
|
smartData.DiskType = normalizeParserType(configuredType)
|
||||||
|
}
|
||||||
|
|
||||||
attributes := make([]*smart.SmartAttribute, 0, 10)
|
attributes := make([]*smart.SmartAttribute, 0, 10)
|
||||||
attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours})
|
attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours})
|
||||||
@@ -1082,9 +1113,12 @@ func (sm *SmartManager) lookupDarwinNvmeCapacity(serial string) uint64 {
|
|||||||
return sm.darwinNvmeCapacity[serial]
|
return sm.darwinNvmeCapacity[serial]
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap
|
// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap.
|
||||||
// Returns hasValidData and exitStatus
|
// configuredType is the user-specified device type (e.g., "nvme") from SMART_DEVICES.
|
||||||
func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
|
// When non-empty, it overrides the disk type reported by smartctl in SmartData.DiskType so that
|
||||||
|
// updateSmartDevices can match entries by the configured composite (name, type) key.
|
||||||
|
// Returns hasValidData and exitStatus.
|
||||||
|
func (sm *SmartManager) parseSmartForNvme(output []byte, configuredType string) (bool, int) {
|
||||||
data := &smart.SmartInfoForNvme{}
|
data := &smart.SmartInfoForNvme{}
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
if err := json.Unmarshal(output, &data); err != nil {
|
||||||
@@ -1128,6 +1162,9 @@ func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
|
|||||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||||
smartData.DiskName = data.Device.Name
|
smartData.DiskName = data.Device.Name
|
||||||
smartData.DiskType = data.Device.Type
|
smartData.DiskType = data.Device.Type
|
||||||
|
if configuredType != "" {
|
||||||
|
smartData.DiskType = normalizeParserType(configuredType)
|
||||||
|
}
|
||||||
|
|
||||||
// nvme attributes does not follow the same format as ata attributes,
|
// nvme attributes does not follow the same format as ata attributes,
|
||||||
// so we manually map each field to SmartAttributes
|
// so we manually map each field to SmartAttributes
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestParseSmartForScsi(t *testing.T) {
|
|||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForScsi(data)
|
hasData, exitStatus := sm.parseSmartForScsi(data, "")
|
||||||
if !hasData {
|
if !hasData {
|
||||||
t.Fatalf("expected SCSI data to parse successfully")
|
t.Fatalf("expected SCSI data to parse successfully")
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func TestParseSmartForSata(t *testing.T) {
|
|||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForSata(data)
|
hasData, exitStatus := sm.parseSmartForSata(data, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 64, exitStatus)
|
assert.Equal(t, 64, exitStatus)
|
||||||
|
|
||||||
@@ -88,6 +88,26 @@ func TestParseSmartForSata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSmartForSataWithConfiguredType(t *testing.T) {
|
||||||
|
fixturePath := filepath.Join("test-data", "smart", "sda.json")
|
||||||
|
data, err := os.ReadFile(fixturePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sm := &SmartManager{
|
||||||
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
|
}
|
||||||
|
|
||||||
|
hasData, exitStatus := sm.parseSmartForSata(data, "sat+megaraid")
|
||||||
|
require.True(t, hasData)
|
||||||
|
assert.Equal(t, 64, exitStatus)
|
||||||
|
|
||||||
|
deviceData, ok := sm.SmartDataMap["9C40918040082"]
|
||||||
|
require.True(t, ok, "expected smart data entry for serial 9C40918040082")
|
||||||
|
|
||||||
|
assert.Equal(t, "sat+megaraid", deviceData.DiskType,
|
||||||
|
"DiskType should be overridden by configuredType")
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
||||||
jsonPayload := []byte(`{
|
jsonPayload := []byte(`{
|
||||||
"smartctl": {"exit_status": 0},
|
"smartctl": {"exit_status": 0},
|
||||||
@@ -112,7 +132,7 @@ func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -147,7 +167,7 @@ func TestParseSmartForSataAtaDeviceStatistics(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -184,7 +204,7 @@ func TestParseSmartForSataNegativeDeviceStatistics(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -223,7 +243,7 @@ func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
|||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -245,7 +265,7 @@ func TestParseSmartForNvme(t *testing.T) {
|
|||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForNvme(data)
|
hasData, exitStatus := sm.parseSmartForNvme(data, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -1225,7 +1245,7 @@ func TestParseSmartForNvmeAppleSSD(t *testing.T) {
|
|||||||
darwinNvmeProvider: fakeProvider,
|
darwinNvmeProvider: fakeProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, _ := sm.parseSmartForNvme(data)
|
hasData, _ := sm.parseSmartForNvme(data, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
|
|
||||||
deviceData, ok := sm.SmartDataMap["0ba0147940253c15"]
|
deviceData, ok := sm.SmartDataMap["0ba0147940253c15"]
|
||||||
@@ -1237,7 +1257,7 @@ func TestParseSmartForNvmeAppleSSD(t *testing.T) {
|
|||||||
assert.Equal(t, 1, providerCalls, "system_profiler should be called once")
|
assert.Equal(t, 1, providerCalls, "system_profiler should be called once")
|
||||||
|
|
||||||
// Second parse: provider should NOT be called again (cache hit)
|
// Second parse: provider should NOT be called again (cache hit)
|
||||||
_, _ = sm.parseSmartForNvme(data)
|
_, _ = sm.parseSmartForNvme(data, "")
|
||||||
assert.Equal(t, 1, providerCalls, "system_profiler should not be called again after caching")
|
assert.Equal(t, 1, providerCalls, "system_profiler should not be called again after caching")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/henrygd/beszel
|
module github.com/henrygd/beszel
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lxzan/gws v1.9.1
|
github.com/lxzan/gws v1.9.1
|
||||||
github.com/nicholas-fedor/shoutrrr v0.14.3
|
github.com/nicholas-fedor/shoutrrr v0.15.1
|
||||||
github.com/pocketbase/dbx v1.12.0
|
github.com/pocketbase/dbx v1.12.0
|
||||||
github.com/pocketbase/pocketbase v0.36.8
|
github.com/pocketbase/pocketbase v0.36.8
|
||||||
github.com/shirou/gopsutil/v4 v4.26.3
|
github.com/shirou/gopsutil/v4 v4.26.3
|
||||||
@@ -18,9 +18,10 @@ require (
|
|||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.52.0
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/net v0.55.0
|
||||||
|
golang.org/x/sys v0.45.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
)
|
)
|
||||||
@@ -46,7 +47,7 @@ require (
|
|||||||
github.com/klauspost/compress v1.18.5 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
@@ -55,12 +56,11 @@ require (
|
|||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/image v0.38.0 // indirect
|
golang.org/x/image v0.41.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/term v0.41.0 // indirect
|
golang.org/x/term v0.43.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
57
go.sum
57
go.sum
@@ -1,7 +1,7 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
@@ -56,8 +56,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M=
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
@@ -81,16 +81,16 @@ github.com/lxzan/gws v1.9.1 h1:4lbIp4cW0hOLP3ejFHR/uWRy741AURx7oKkNNi2OT9o=
|
|||||||
github.com/lxzan/gws v1.9.1/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
|
github.com/lxzan/gws v1.9.1/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.14.3 h1:aBX2iw9a7jl5wfHd3bi9LnS5ucoYIy6KcLH9XVF+gig=
|
github.com/nicholas-fedor/shoutrrr v0.15.1 h1:dfgqpaeyr0CwUhqtwWBHS4girmAvFPOoxroHaVH1q1Y=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.14.3/go.mod h1:U7IywBkLpBV7rgn8iLbQ9/LklJG1gm24bFv5cXXsDKs=
|
github.com/nicholas-fedor/shoutrrr v0.15.1/go.mod h1:xrdV1ab2W0/xa5kM6WP9mBuqVuJaDWqUwYvYvVuPTjk=
|
||||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag=
|
||||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA=
|
||||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@@ -133,18 +133,18 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
@@ -153,18 +153,17 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -195,6 +195,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := a.Start(serverConfig); err != nil {
|
if err := a.Start(serverConfig); err != nil {
|
||||||
log.Fatal("Failed to start server: ", err)
|
log.Fatal("Failed to start: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function LineChartDefault({
|
|||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
// stackId={dataPoint.stackId}
|
// stackId={dataPoint.stackId}
|
||||||
order={dataPoint.order || i}
|
order={dataPoint.order || i}
|
||||||
// activeDot={dataPoint.activeDot ?? true}
|
activeDot={dataPoint.activeDot ?? true}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { toast } from "../ui/use-toast"
|
|||||||
import { OtpInputForm } from "./otp-forms"
|
import { OtpInputForm } from "./otp-forms"
|
||||||
|
|
||||||
const honeypot = v.literal("")
|
const honeypot = v.literal("")
|
||||||
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
|
const emailSchema = v.pipe(v.string(), v.rfcEmail(t`Invalid email address.`))
|
||||||
const passwordSchema = v.pipe(
|
const passwordSchema = v.pipe(
|
||||||
v.string(),
|
v.string(),
|
||||||
v.minLength(8, t`Password must be at least 8 characters.`),
|
v.minLength(8, t`Password must be at least 8 characters.`),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface ShoutrrrUrlCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NotificationSchema = v.object({
|
const NotificationSchema = v.object({
|
||||||
emails: v.array(v.pipe(v.string(), v.email())),
|
emails: v.array(v.pipe(v.string(), v.rfcEmail())),
|
||||||
webhooks: v.array(v.pipe(v.string(), v.url())),
|
webhooks: v.array(v.pipe(v.string(), v.url())),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ export function TemperatureChart({
|
|||||||
label: key,
|
label: key,
|
||||||
dataKey: dataKeys[key],
|
dataKey: dataKeys[key],
|
||||||
color: colorMap[key],
|
color: colorMap[key],
|
||||||
opacity: strokeOpacity,
|
strokeOpacity,
|
||||||
|
activeDot: !filtered,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [sortedKeys, filter, dataKeys, colorMap])
|
}, [sortedKeys, filter, dataKeys, colorMap])
|
||||||
@@ -134,7 +135,7 @@ export function TemperatureChart({
|
|||||||
// label: `Test ${++i}`,
|
// label: `Test ${++i}`,
|
||||||
// dataKey: () => 0,
|
// dataKey: () => 0,
|
||||||
// color: "red",
|
// color: "red",
|
||||||
// opacity: 1,
|
// strokeOpacity: 1,
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
@@ -202,6 +203,7 @@ export function TemperatureChart({
|
|||||||
return `${decimalString(value)} ${unit}`
|
return `${decimalString(value)} ${unit}`
|
||||||
}}
|
}}
|
||||||
dataPoints={dataPoints}
|
dataPoints={dataPoints}
|
||||||
|
filter={filter}
|
||||||
></LineChartDefault>
|
></LineChartDefault>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
|
# Specialized images available for Nvidia / Intel GPUs
|
||||||
|
#
|
||||||
|
# Docs: https://beszel.dev/guide/agent-installation
|
||||||
|
# Env vars: https://beszel.dev/guide/environment-variables
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel-agent:
|
beszel-agent:
|
||||||
image: 'henrygd/beszel-agent' #Or henrygd/beszel-agent-nvidia
|
image: henrygd/beszel-agent
|
||||||
container_name: 'beszel-agent'
|
container_name: beszel-agent
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
# Only when using henrygd/beszel-agent-nvidia
|
|
||||||
# runtime: nvidia
|
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./beszel_agent_data:/var/lib/beszel-agent
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||||
# - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
|
# - /mnt/disk1/.beszel:/extra-filesystems/disk1:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
LISTEN: 45876
|
||||||
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
KEY: "<public key>"
|
||||||
# Only when using henrygd/beszel-agent-nvidia
|
HUB_URL: "<hub url>"
|
||||||
# NVIDIA_VISIBLE_DEVICES: all
|
TOKEN: "<token>"
|
||||||
# NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/agent', 'health']
|
||||||
|
# interval: 120s
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
# Docs: https://beszel.dev/guide/hub-installation
|
||||||
|
# Env vars: https://beszel.dev/guide/environment-variables
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel:
|
beszel:
|
||||||
image: 'henrygd/beszel'
|
image: "henrygd/beszel"
|
||||||
container_name: 'beszel'
|
container_name: "beszel"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '8090:8090'
|
- "8090:8090"
|
||||||
volumes:
|
volumes:
|
||||||
- ./beszel_data:/beszel_data
|
- ./beszel_data:/beszel_data
|
||||||
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
|
||||||
|
# interval: 120s
|
||||||
|
# start_period: 10s
|
||||||
|
# timeout: 5s
|
||||||
|
|||||||
@@ -1,26 +1,38 @@
|
|||||||
|
# Docs: https://beszel.dev/guide/getting-started
|
||||||
|
# Env vars: https://beszel.dev/guide/environment-variables
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel:
|
beszel:
|
||||||
image: 'henrygd/beszel'
|
image: henrygd/beszel:latest
|
||||||
container_name: 'beszel'
|
container_name: beszel
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
APP_URL: http://localhost:8090
|
||||||
ports:
|
ports:
|
||||||
- '8090:8090'
|
- 8090:8090
|
||||||
volumes:
|
volumes:
|
||||||
- ./beszel_data:/beszel_data
|
- ./beszel_data:/beszel_data
|
||||||
extra_hosts:
|
- ./beszel_socket:/beszel_socket
|
||||||
- 'host.docker.internal:host-gateway'
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
|
||||||
|
# interval: 120s
|
||||||
|
# start_period: 10s
|
||||||
|
# timeout: 5s
|
||||||
|
|
||||||
beszel-agent:
|
beszel-agent:
|
||||||
image: 'henrygd/beszel-agent' #Add -nvidia for nvidia gpus
|
image: henrygd/beszel-agent:latest
|
||||||
container_name: 'beszel-agent'
|
container_name: beszel-agent
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
# runtime: nvidia # when using beszel-agent-nvidia
|
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./beszel_agent_data:/var/lib/beszel-agent
|
||||||
|
- ./beszel_socket:/beszel_socket
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
LISTEN: /beszel_socket/beszel.sock
|
||||||
KEY: '...'
|
HUB_URL: http://localhost:8090
|
||||||
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats
|
TOKEN: <token>
|
||||||
# NVIDIA_VISIBLE_DEVICES: all # when using beszel-agent-nvidia
|
KEY: "<key>"
|
||||||
# NVIDIA_DRIVER_CAPABILITIES: utility # when using beszel-agent-nvidia
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/agent', 'health']
|
||||||
|
# interval: 120s
|
||||||
|
|||||||
Reference in New Issue
Block a user