mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-24 14:36:17 +01:00
Compare commits
31 Commits
eb4bdafbea
...
split-inte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb26877720 | ||
|
|
e149366451 | ||
|
|
8da1ded73e | ||
|
|
efa37b2312 | ||
|
|
bcdb4c92b5 | ||
|
|
a7d07310b6 | ||
|
|
8db87e5497 | ||
|
|
e601a0d564 | ||
|
|
07491108cd | ||
|
|
42ab17de1f | ||
|
|
2d14174f61 | ||
|
|
a19ccc9263 | ||
|
|
956880aa59 | ||
|
|
b2b54db409 | ||
|
|
32d5188eef | ||
|
|
46dab7f531 | ||
|
|
c898a9ebbc | ||
|
|
8a13b05c20 | ||
|
|
86ea23fe39 | ||
|
|
a284dd74dd | ||
|
|
6a0075291c | ||
|
|
f542bc70a1 | ||
|
|
270e59d9ea | ||
|
|
0d97a604f8 | ||
|
|
f6078fc232 | ||
|
|
6f5d95031c | ||
|
|
4e26defdca | ||
|
|
cda8fa7efd | ||
|
|
fd050f2a8f | ||
|
|
e53d41dcec | ||
|
|
a1eb15dabb |
48
.dockerignore
Normal file
48
.dockerignore
Normal file
@@ -0,0 +1,48 @@
|
||||
# Node.js dependencies
|
||||
node_modules
|
||||
internalsite/node_modules
|
||||
|
||||
# Go build artifacts and binaries
|
||||
build
|
||||
dist
|
||||
*.exe
|
||||
beszel-agent
|
||||
beszel_data*
|
||||
pb_data
|
||||
data
|
||||
temp
|
||||
|
||||
# Development and IDE files
|
||||
.vscode
|
||||
.idea*
|
||||
*.swc
|
||||
__debug_*
|
||||
|
||||
# Git and version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation and supplemental files
|
||||
*.md
|
||||
supplemental
|
||||
freebsd-port
|
||||
|
||||
# Test files (exclude from production builds)
|
||||
*_test.go
|
||||
coverage
|
||||
|
||||
# Docker files
|
||||
dockerfile_*
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.log
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# .NET build artifacts
|
||||
agent/lhm/obj
|
||||
agent/lhm/bin
|
||||
32
.github/workflows/docker-images.yml
vendored
32
.github/workflows/docker-images.yml
vendored
@@ -13,44 +13,44 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- image: henrygd/beszel
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_hub
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_hub
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
- image: henrygd/beszel-agent
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
- image: henrygd/beszel-agent-nvidia
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent_nvidia
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||
platforms: linux/amd64
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
- image: ghcr.io/${{ github.repository }}/beszel
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_hub
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_hub
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent_nvidia
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||
platforms: linux/amd64
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -68,10 +68,10 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --no-save --cwd ./beszel/site
|
||||
run: bun install --no-save --cwd ./internal/site
|
||||
|
||||
- name: Build site
|
||||
run: bun run --cwd ./beszel/site build
|
||||
run: bun run --cwd ./internal/site build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -93,8 +93,8 @@ jobs:
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to Docker Hub
|
||||
env:
|
||||
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
|
||||
env:
|
||||
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
|
||||
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --no-save --cwd ./beszel/site
|
||||
run: bun install --no-save --cwd ./internal/site
|
||||
|
||||
- name: Build site
|
||||
run: bun run --cwd ./beszel/site build
|
||||
run: bun run --cwd ./internal/site build
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
|
||||
- name: Build .NET LHM executable for Windows sensors
|
||||
run: |
|
||||
dotnet build -c Release ./beszel/internal/agent/lhm/beszel_lhm.csproj
|
||||
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj
|
||||
shell: bash
|
||||
|
||||
- name: GoReleaser beszel
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
workdir: ./beszel
|
||||
workdir: ./
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
||||
2
.github/workflows/vulncheck.yml
vendored
2
.github/workflows/vulncheck.yml
vendored
@@ -29,5 +29,5 @@ jobs:
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
shell: bash
|
||||
- name: Run govulncheck
|
||||
run: govulncheck -C ./beszel -show verbose ./...
|
||||
run: govulncheck -show verbose ./...
|
||||
shell: bash
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -8,15 +8,16 @@ beszel_data
|
||||
beszel_data*
|
||||
dist
|
||||
*.exe
|
||||
beszel/cmd/hub/hub
|
||||
beszel/cmd/agent/agent
|
||||
internal/cmd/hub/hub
|
||||
internal/cmd/agent/agent
|
||||
node_modules
|
||||
beszel/build
|
||||
build
|
||||
*timestamp*
|
||||
.swc
|
||||
beszel/site/src/locales/**/*.ts
|
||||
internal/site/src/locales/**/*.ts
|
||||
*.bak
|
||||
__debug_*
|
||||
beszel/internal/agent/lhm/obj
|
||||
beszel/internal/agent/lhm/bin
|
||||
agent/lhm/obj
|
||||
agent/lhm/bin
|
||||
dockerfile_agent_dev
|
||||
.vite
|
||||
@@ -9,7 +9,7 @@ before:
|
||||
builds:
|
||||
- id: beszel
|
||||
binary: beszel
|
||||
main: cmd/hub/hub.go
|
||||
main: internal/cmd/hub/hub.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
@@ -22,7 +22,7 @@ builds:
|
||||
|
||||
- id: beszel-agent
|
||||
binary: beszel-agent
|
||||
main: cmd/agent/agent.go
|
||||
main: internal/cmd/agent/agent.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
@@ -103,28 +103,28 @@ nfpms:
|
||||
formats:
|
||||
- deb
|
||||
contents:
|
||||
- src: ../supplemental/debian/beszel-agent.service
|
||||
- src: ./supplemental/debian/beszel-agent.service
|
||||
dst: lib/systemd/system/beszel-agent.service
|
||||
packager: deb
|
||||
- src: ../supplemental/debian/copyright
|
||||
- src: ./supplemental/debian/copyright
|
||||
dst: usr/share/doc/beszel-agent/copyright
|
||||
packager: deb
|
||||
- src: ../supplemental/debian/lintian-overrides
|
||||
- src: ./supplemental/debian/lintian-overrides
|
||||
dst: usr/share/lintian/overrides/beszel-agent
|
||||
packager: deb
|
||||
scripts:
|
||||
postinstall: ../supplemental/debian/postinstall.sh
|
||||
preremove: ../supplemental/debian/prerm.sh
|
||||
postremove: ../supplemental/debian/postrm.sh
|
||||
postinstall: ./supplemental/debian/postinstall.sh
|
||||
preremove: ./supplemental/debian/prerm.sh
|
||||
postremove: ./supplemental/debian/postrm.sh
|
||||
deb:
|
||||
predepends:
|
||||
- adduser
|
||||
- debconf
|
||||
scripts:
|
||||
templates: ../supplemental/debian/templates
|
||||
templates: ./supplemental/debian/templates
|
||||
# Currently broken due to a bug in goreleaser
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
#config: ../supplemental/debian/config.sh
|
||||
#config: ./supplemental/debian/config.sh
|
||||
|
||||
scoops:
|
||||
- ids: [beszel-agent]
|
||||
@@ -26,11 +26,11 @@ tidy:
|
||||
|
||||
build-web-ui:
|
||||
@if command -v bun >/dev/null 2>&1; then \
|
||||
bun install --cwd ./site && \
|
||||
bun run --cwd ./site build; \
|
||||
bun install --cwd ./internal/site && \
|
||||
bun run --cwd ./internal/site build; \
|
||||
else \
|
||||
npm install --prefix ./site && \
|
||||
npm run --prefix ./site build; \
|
||||
npm install --prefix ./internal/site && \
|
||||
npm run --prefix ./internal/site build; \
|
||||
fi
|
||||
|
||||
# Conditional .NET build - only for Windows
|
||||
@@ -38,8 +38,8 @@ build-dotnet-conditional:
|
||||
@if [ "$(OS)" = "windows" ]; then \
|
||||
echo "Building .NET executable for Windows..."; \
|
||||
if command -v dotnet >/dev/null 2>&1; then \
|
||||
rm -rf ./internal/agent/lhm/bin; \
|
||||
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
|
||||
rm -rf ./agent/lhm/bin; \
|
||||
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
|
||||
else \
|
||||
echo "Error: dotnet not found. Install .NET SDK to build Windows agent."; \
|
||||
exit 1; \
|
||||
@@ -48,51 +48,51 @@ build-dotnet-conditional:
|
||||
|
||||
# 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
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/agent
|
||||
|
||||
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)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
|
||||
|
||||
build-hub-dev: tidy
|
||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
|
||||
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
|
||||
|
||||
build: build-agent build-hub
|
||||
|
||||
generate-locales:
|
||||
@if [ ! -f ./site/src/locales/en/en.ts ]; then \
|
||||
@if [ ! -f ./internal/site/src/locales/en/en.ts ]; then \
|
||||
echo "Generating locales..."; \
|
||||
command -v bun >/dev/null 2>&1 && cd ./site && bun install && bun run sync || cd ./site && npm install && npm run sync; \
|
||||
command -v bun >/dev/null 2>&1 && cd ./internal/site && bun install && bun run sync || cd ./internal/site && npm install && npm run sync; \
|
||||
fi
|
||||
|
||||
dev-server: generate-locales
|
||||
cd ./site
|
||||
cd ./internal/site
|
||||
@if command -v bun >/dev/null 2>&1; then \
|
||||
cd ./site && bun run dev --host 0.0.0.0; \
|
||||
cd ./internal/site && bun run dev --host 0.0.0.0; \
|
||||
else \
|
||||
cd ./site && npm run dev --host 0.0.0.0; \
|
||||
cd ./internal/site && npm run dev --host 0.0.0.0; \
|
||||
fi
|
||||
|
||||
dev-hub: export ENV=dev
|
||||
dev-hub:
|
||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||
find ./internal -type f -name '*.go' | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||
else \
|
||||
cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
||||
cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
||||
fi
|
||||
|
||||
dev-agent:
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
find ./cmd/agent/*.go ./internal/agent/*.go | entr -r go run beszel/cmd/agent; \
|
||||
find ./internal/cmd/agent/*.go ./agent/*.go | entr -r go run github.com/henrygd/beszel/internal/cmd/agent; \
|
||||
else \
|
||||
go run beszel/cmd/agent; \
|
||||
go run github.com/henrygd/beszel/internal/cmd/agent; \
|
||||
fi
|
||||
|
||||
build-dotnet:
|
||||
@if command -v dotnet >/dev/null 2>&1; then \
|
||||
rm -rf ./internal/agent/lhm/bin; \
|
||||
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
|
||||
rm -rf ./agent/lhm/bin; \
|
||||
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
|
||||
else \
|
||||
echo "dotnet not found"; \
|
||||
fi
|
||||
@@ -1,9 +1,10 @@
|
||||
// Package agent handles the agent's SSH server and system stats collection.
|
||||
// Package agent implements the Beszel monitoring agent that collects and serves system metrics.
|
||||
//
|
||||
// The agent runs on monitored systems and communicates collected data
|
||||
// to the Beszel hub for centralized monitoring and alerting.
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/entities/system"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log/slog"
|
||||
@@ -14,28 +15,30 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
sync.Mutex // Used to lock agent while collecting data
|
||||
debug bool // true if LOG_LEVEL is set to debug
|
||||
zfs bool // true if system has arcstats
|
||||
memCalc string // Memory calculation formula
|
||||
fsNames []string // List of filesystem device names being monitored
|
||||
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
|
||||
netInterfaces map[string]struct{} // Stores all valid network interfaces
|
||||
netIoStats system.NetIoStats // Keeps track of bandwidth usage
|
||||
dockerManager *dockerManager // Manages Docker API requests
|
||||
sensorConfig *SensorConfig // Sensors config
|
||||
systemInfo system.Info // Host system info
|
||||
gpuManager *GPUManager // Manages GPU data
|
||||
cache *SessionCache // Cache for system stats based on primary session ID
|
||||
connectionManager *ConnectionManager // Channel to signal connection events
|
||||
server *ssh.Server // SSH server
|
||||
dataDir string // Directory for persisting data
|
||||
keys []gossh.PublicKey // SSH public keys
|
||||
sync.Mutex // Used to lock agent while collecting data
|
||||
debug bool // true if LOG_LEVEL is set to debug
|
||||
zfs bool // true if system has arcstats
|
||||
memCalc string // Memory calculation formula
|
||||
fsNames []string // List of filesystem device names being monitored
|
||||
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
|
||||
netInterfaces map[string]struct{} // Stores all valid network interfaces
|
||||
netIoStats map[string]system.NetIoStats // Keeps track of per-interface bandwidth usage
|
||||
dockerManager *dockerManager // Manages Docker API requests
|
||||
sensorConfig *SensorConfig // Sensors config
|
||||
systemInfo system.Info // Host system info
|
||||
gpuManager *GPUManager // Manages GPU data
|
||||
cache *SessionCache // Cache for system stats based on primary session ID
|
||||
connectionManager *ConnectionManager // Channel to signal connection events
|
||||
server *ssh.Server // SSH server
|
||||
dataDir string // Directory for persisting data
|
||||
keys []gossh.PublicKey // SSH public keys
|
||||
}
|
||||
|
||||
// NewAgent creates a new agent with the given data directory for persisting data.
|
||||
@@ -1,8 +1,9 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
// Not thread safe since we only access from gatherStats which is already locked
|
||||
@@ -4,11 +4,12 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -1,8 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -15,6 +13,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/lxzan/gws"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -84,7 +85,7 @@ func getToken() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(tokenBytes), nil
|
||||
return strings.TrimSpace(string(tokenBytes)), nil
|
||||
}
|
||||
|
||||
// getOptions returns the WebSocket client options, creating them if necessary.
|
||||
@@ -4,8 +4,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"crypto/ed25519"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -13,6 +11,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -535,4 +537,25 @@ func TestGetToken(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", token, "Empty file should return empty string")
|
||||
})
|
||||
|
||||
t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) {
|
||||
unsetEnvVars()
|
||||
|
||||
tokenWithWhitespace := " test-token-with-whitespace \n\t"
|
||||
expectedToken := "test-token-with-whitespace"
|
||||
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tokenFile.Name())
|
||||
|
||||
_, err = tokenFile.WriteString(tokenWithWhitespace)
|
||||
require.NoError(t, err)
|
||||
tokenFile.Close()
|
||||
|
||||
os.Setenv("TOKEN_FILE", tokenFile.Name())
|
||||
defer os.Unsetenv("TOKEN_FILE")
|
||||
|
||||
token, err := getToken()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content")
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/agent/health"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/agent/health"
|
||||
)
|
||||
|
||||
// ConnectionManager manages the connection state and events for the agent.
|
||||
@@ -1,7 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
|
||||
"github.com/blang/semver"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -5,12 +5,16 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
func (a *Agent) initializeNetIoStats() {
|
||||
// reset valid network interfaces
|
||||
a.netInterfaces = make(map[string]struct{}, 0)
|
||||
// reset network I/O stats per interface
|
||||
a.netIoStats = make(map[string]system.NetIoStats, 0)
|
||||
|
||||
// map of network interface names passed in via NICS env var
|
||||
var nicsMap map[string]struct{}
|
||||
@@ -22,13 +26,10 @@ func (a *Agent) initializeNetIoStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// reset network I/O stats
|
||||
a.netIoStats.BytesSent = 0
|
||||
a.netIoStats.BytesRecv = 0
|
||||
|
||||
// get intial network I/O stats
|
||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||
a.netIoStats.Time = time.Now()
|
||||
now := time.Now()
|
||||
|
||||
for _, v := range netIO {
|
||||
switch {
|
||||
// skip if nics exists and the interface is not in the list
|
||||
@@ -43,10 +44,15 @@ func (a *Agent) initializeNetIoStats() {
|
||||
}
|
||||
}
|
||||
slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv)
|
||||
a.netIoStats.BytesSent += v.BytesSent
|
||||
a.netIoStats.BytesRecv += v.BytesRecv
|
||||
// store as a valid network interface
|
||||
a.netInterfaces[v.Name] = struct{}{}
|
||||
// initialize per-interface stats
|
||||
a.netIoStats[v.Name] = system.NetIoStats{
|
||||
BytesRecv: v.BytesRecv,
|
||||
BytesSent: v.BytesSent,
|
||||
Time: now,
|
||||
Name: v.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -11,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
@@ -4,12 +4,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -1,9 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/entities/system"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,6 +11,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -1,8 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
@@ -15,6 +13,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -1,9 +1,6 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/agent/battery"
|
||||
"beszel/internal/entities/system"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -12,6 +9,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/agent/battery"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
@@ -175,53 +176,85 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
if len(a.netInterfaces) == 0 {
|
||||
// if no network interfaces, initialize again
|
||||
// this is a fix if agent started before network is online (#466)
|
||||
// maybe refactor this in the future to not cache interface names at all so we
|
||||
// don't miss an interface that's been added after agent started in any circumstance
|
||||
a.initializeNetIoStats()
|
||||
}
|
||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||
msElapsed := uint64(time.Since(a.netIoStats.Time).Milliseconds())
|
||||
a.netIoStats.Time = time.Now()
|
||||
totalBytesSent := uint64(0)
|
||||
totalBytesRecv := uint64(0)
|
||||
// sum all bytes sent and received
|
||||
now := time.Now()
|
||||
|
||||
// pre-allocate maps with known capacity
|
||||
interfaceCount := len(a.netInterfaces)
|
||||
if systemStats.NetworkInterfaces == nil || len(systemStats.NetworkInterfaces) != interfaceCount {
|
||||
systemStats.NetworkInterfaces = make(map[string]system.NetworkInterfaceStats, interfaceCount)
|
||||
}
|
||||
|
||||
var totalSent, totalRecv float64
|
||||
|
||||
// single pass through interfaces
|
||||
for _, v := range netIO {
|
||||
// skip if not in valid network interfaces list
|
||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||
continue
|
||||
}
|
||||
totalBytesSent += v.BytesSent
|
||||
totalBytesRecv += v.BytesRecv
|
||||
}
|
||||
// add to systemStats
|
||||
var bytesSentPerSecond, bytesRecvPerSecond uint64
|
||||
if msElapsed > 0 {
|
||||
bytesSentPerSecond = (totalBytesSent - a.netIoStats.BytesSent) * 1000 / msElapsed
|
||||
bytesRecvPerSecond = (totalBytesRecv - a.netIoStats.BytesRecv) * 1000 / msElapsed
|
||||
}
|
||||
networkSentPs := bytesToMegabytes(float64(bytesSentPerSecond))
|
||||
networkRecvPs := bytesToMegabytes(float64(bytesRecvPerSecond))
|
||||
// add check for issue (#150) where sent is a massive number
|
||||
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
|
||||
slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
|
||||
for _, v := range netIO {
|
||||
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||
continue
|
||||
|
||||
// get previous stats for this interface
|
||||
prevStats, exists := a.netIoStats[v.Name]
|
||||
var networkSentPs, networkRecvPs float64
|
||||
|
||||
if exists {
|
||||
secondsElapsed := time.Since(prevStats.Time).Seconds()
|
||||
if secondsElapsed > 0 {
|
||||
// direct calculation to MB/s, avoiding intermediate bytes/sec
|
||||
networkSentPs = bytesToMegabytes(float64(v.BytesSent-prevStats.BytesSent) / secondsElapsed)
|
||||
networkRecvPs = bytesToMegabytes(float64(v.BytesRecv-prevStats.BytesRecv) / secondsElapsed)
|
||||
}
|
||||
slog.Info(v.Name, "recv", v.BytesRecv, "sent", v.BytesSent)
|
||||
}
|
||||
|
||||
// accumulate totals
|
||||
totalSent += networkSentPs
|
||||
totalRecv += networkRecvPs
|
||||
|
||||
// store per-interface stats
|
||||
systemStats.NetworkInterfaces[v.Name] = system.NetworkInterfaceStats{
|
||||
NetworkSent: networkSentPs,
|
||||
NetworkRecv: networkRecvPs,
|
||||
TotalBytesSent: v.BytesSent,
|
||||
TotalBytesRecv: v.BytesRecv,
|
||||
}
|
||||
|
||||
// update previous stats (reuse existing struct if possible)
|
||||
if prevStats.Name == v.Name {
|
||||
prevStats.BytesRecv = v.BytesRecv
|
||||
prevStats.BytesSent = v.BytesSent
|
||||
prevStats.PacketsSent = v.PacketsSent
|
||||
prevStats.PacketsRecv = v.PacketsRecv
|
||||
prevStats.Time = now
|
||||
a.netIoStats[v.Name] = prevStats
|
||||
} else {
|
||||
a.netIoStats[v.Name] = system.NetIoStats{
|
||||
BytesRecv: v.BytesRecv,
|
||||
BytesSent: v.BytesSent,
|
||||
PacketsSent: v.PacketsSent,
|
||||
PacketsRecv: v.PacketsRecv,
|
||||
Time: now,
|
||||
Name: v.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add check for issue (#150) where sent is a massive number
|
||||
if totalSent > 10_000 || totalRecv > 10_000 {
|
||||
slog.Warn("Invalid net stats. Resetting.", "sent", totalSent, "recv", totalRecv)
|
||||
// reset network I/O stats
|
||||
a.initializeNetIoStats()
|
||||
} else {
|
||||
systemStats.NetworkSent = networkSentPs
|
||||
systemStats.NetworkRecv = networkRecvPs
|
||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
|
||||
// update netIoStats
|
||||
a.netIoStats.BytesSent = totalBytesSent
|
||||
a.netIoStats.BytesRecv = totalBytesRecv
|
||||
systemStats.NetworkSent = totalSent
|
||||
systemStats.NetworkRecv = totalRecv
|
||||
}
|
||||
}
|
||||
|
||||
// connection counts
|
||||
a.updateConnectionCounts(&systemStats)
|
||||
|
||||
// temperatures
|
||||
// TODO: maybe refactor to methods on systemStats
|
||||
a.updateTemperatures(&systemStats)
|
||||
@@ -269,14 +302,109 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
a.systemInfo.MemPct = systemStats.MemPct
|
||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||
a.systemInfo.Uptime, _ = host.Uptime()
|
||||
// TODO: in future release, remove MB bandwidth values in favor of bytes
|
||||
a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv)
|
||||
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
||||
|
||||
// Sum all per-interface network sent/recv and assign to systemInfo
|
||||
var totalSent, totalRecv float64
|
||||
for _, iface := range systemStats.NetworkInterfaces {
|
||||
totalSent += iface.NetworkSent
|
||||
totalRecv += iface.NetworkRecv
|
||||
}
|
||||
a.systemInfo.NetworkSent = twoDecimals(totalSent)
|
||||
a.systemInfo.NetworkRecv = twoDecimals(totalRecv)
|
||||
slog.Debug("sysinfo", "data", a.systemInfo)
|
||||
|
||||
return systemStats
|
||||
}
|
||||
|
||||
func (a *Agent) updateConnectionCounts(systemStats *system.Stats) {
|
||||
// Get IPv4 connections
|
||||
connectionsIPv4, err := psutilNet.Connections("inet")
|
||||
if err != nil {
|
||||
slog.Debug("Failed to get IPv4 connection stats", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get IPv6 connections
|
||||
connectionsIPv6, err := psutilNet.Connections("inet6")
|
||||
if err != nil {
|
||||
slog.Debug("Failed to get IPv6 connection stats", "err", err)
|
||||
// Continue with IPv4 only if IPv6 fails
|
||||
}
|
||||
|
||||
// Initialize Nets map if needed
|
||||
if systemStats.Nets == nil {
|
||||
systemStats.Nets = make(map[string]float64)
|
||||
}
|
||||
|
||||
// Count IPv4 connection states
|
||||
connStatsIPv4 := map[string]int{
|
||||
"established": 0,
|
||||
"listen": 0,
|
||||
"time_wait": 0,
|
||||
"close_wait": 0,
|
||||
"syn_recv": 0,
|
||||
}
|
||||
|
||||
for _, conn := range connectionsIPv4 {
|
||||
// Only count TCP connections (Type 1 = SOCK_STREAM)
|
||||
if conn.Type == 1 {
|
||||
switch strings.ToUpper(conn.Status) {
|
||||
case "ESTABLISHED":
|
||||
connStatsIPv4["established"]++
|
||||
case "LISTEN":
|
||||
connStatsIPv4["listen"]++
|
||||
case "TIME_WAIT":
|
||||
connStatsIPv4["time_wait"]++
|
||||
case "CLOSE_WAIT":
|
||||
connStatsIPv4["close_wait"]++
|
||||
case "SYN_RECV":
|
||||
connStatsIPv4["syn_recv"]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count IPv6 connection states
|
||||
connStatsIPv6 := map[string]int{
|
||||
"established": 0,
|
||||
"listen": 0,
|
||||
"time_wait": 0,
|
||||
"close_wait": 0,
|
||||
"syn_recv": 0,
|
||||
}
|
||||
|
||||
for _, conn := range connectionsIPv6 {
|
||||
// Only count TCP connections (Type 1 = SOCK_STREAM)
|
||||
if conn.Type == 1 {
|
||||
switch strings.ToUpper(conn.Status) {
|
||||
case "ESTABLISHED":
|
||||
connStatsIPv6["established"]++
|
||||
case "LISTEN":
|
||||
connStatsIPv6["listen"]++
|
||||
case "TIME_WAIT":
|
||||
connStatsIPv6["time_wait"]++
|
||||
case "CLOSE_WAIT":
|
||||
connStatsIPv6["close_wait"]++
|
||||
case "SYN_RECV":
|
||||
connStatsIPv6["syn_recv"]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add IPv4 connection counts to Nets
|
||||
systemStats.Nets["conn_established"] = float64(connStatsIPv4["established"])
|
||||
systemStats.Nets["conn_listen"] = float64(connStatsIPv4["listen"])
|
||||
systemStats.Nets["conn_timewait"] = float64(connStatsIPv4["time_wait"])
|
||||
systemStats.Nets["conn_closewait"] = float64(connStatsIPv4["close_wait"])
|
||||
systemStats.Nets["conn_synrecv"] = float64(connStatsIPv4["syn_recv"])
|
||||
|
||||
// Add IPv6 connection counts to Nets
|
||||
systemStats.Nets["conn6_established"] = float64(connStatsIPv6["established"])
|
||||
systemStats.Nets["conn6_listen"] = float64(connStatsIPv6["listen"])
|
||||
systemStats.Nets["conn6_timewait"] = float64(connStatsIPv6["time_wait"])
|
||||
systemStats.Nets["conn6_closewait"] = float64(connStatsIPv6["close_wait"])
|
||||
systemStats.Nets["conn6_synrecv"] = float64(connStatsIPv6["syn_recv"])
|
||||
}
|
||||
|
||||
// Returns the size of the ZFS ARC memory cache in bytes
|
||||
func getARCSize() (uint64, error) {
|
||||
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
|
||||
@@ -1,13 +1,14 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/ghupdate"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel/internal/ghupdate"
|
||||
)
|
||||
|
||||
// restarter knows how to restart the beszel-agent service.
|
||||
15
beszel.go
Normal file
15
beszel.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package beszel provides core application constants and version information
|
||||
// which are used throughout the application.
|
||||
package beszel
|
||||
|
||||
import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
// Version is the current version of the application.
|
||||
Version = "0.12.7"
|
||||
// AppName is the name of the application.
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
// MinVersionCbor is the minimum supported version for CBOR compatibility.
|
||||
var MinVersionCbor = semver.MustParse("0.12.0")
|
||||
@@ -1,114 +0,0 @@
|
||||
package system
|
||||
|
||||
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
||||
MaxCpu float64 `json:"cpum,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"4,keyasint"`
|
||||
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
|
||||
MemZfsArc float64 `json:"mz,omitempty" cbor:"6,keyasint,omitempty"` // ZFS ARC memory
|
||||
Swap float64 `json:"s,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
SwapUsed float64 `json:"su,omitempty" cbor:"8,keyasint,omitempty"`
|
||||
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
|
||||
DiskReadPs float64 `json:"dr" cbor:"12,keyasint"`
|
||||
DiskWritePs float64 `json:"dw" cbor:"13,keyasint"`
|
||||
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"14,keyasint,omitempty"`
|
||||
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
NetworkSent float64 `json:"ns" cbor:"16,keyasint"`
|
||||
NetworkRecv float64 `json:"nr" cbor:"17,keyasint"`
|
||||
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"18,keyasint,omitempty"`
|
||||
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"19,keyasint,omitempty"`
|
||||
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
|
||||
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
|
||||
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
// TODO: remove other load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Temperature float64 `json:"-"`
|
||||
MemoryUsed float64 `json:"mu,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
MemoryTotal float64 `json:"mt,omitempty" cbor:"2,keyasint,omitempty"`
|
||||
Usage float64 `json:"u" cbor:"3,keyasint"`
|
||||
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
Count float64 `json:"-"`
|
||||
}
|
||||
|
||||
type FsStats struct {
|
||||
Time time.Time `json:"-"`
|
||||
Root bool `json:"-"`
|
||||
Mountpoint string `json:"-"`
|
||||
DiskTotal float64 `json:"d" cbor:"0,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"1,keyasint"`
|
||||
TotalRead uint64 `json:"-"`
|
||||
TotalWrite uint64 `json:"-"`
|
||||
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
|
||||
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
|
||||
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"5,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type NetIoStats struct {
|
||||
BytesRecv uint64
|
||||
BytesSent uint64
|
||||
Time time.Time
|
||||
Name string
|
||||
}
|
||||
|
||||
type Os = uint8
|
||||
|
||||
const (
|
||||
Linux Os = iota
|
||||
Darwin
|
||||
Windows
|
||||
Freebsd
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Hostname string `json:"h" cbor:"0,keyasint"`
|
||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
Cores int `json:"c" cbor:"2,keyasint"`
|
||||
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
CpuModel string `json:"m" cbor:"4,keyasint"`
|
||||
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
|
||||
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
||||
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
|
||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||
Os Os `json:"os" cbor:"14,keyasint"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||
// TODO: remove load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
type CombinedData struct {
|
||||
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
||||
Info Info `json:"info" cbor:"1,keyasint"`
|
||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||
}
|
||||
Binary file not shown.
@@ -1,24 +0,0 @@
|
||||
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package beszel
|
||||
|
||||
import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
Version = "0.12.7"
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
var MinVersionCbor = semver.MustParse("0.12.0")
|
||||
@@ -1,4 +1,4 @@
|
||||
module beszel
|
||||
module github.com/henrygd/beszel
|
||||
|
||||
go 1.25.1
|
||||
|
||||
4
i18n.yml
4
i18n.yml
@@ -1,3 +1,3 @@
|
||||
files:
|
||||
- source: /beszel/site/src/locales/en/en.po
|
||||
translation: /beszel/site/src/locales/%two_letters_code%/%two_letters_code%.po
|
||||
- source: /internal/site/src/locales/en/
|
||||
translation: /internal/site/src/locales/%two_letters_code%/%two_letters_code%.po
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
@@ -37,7 +38,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
case "Memory":
|
||||
val = data.Info.MemPct
|
||||
case "Bandwidth":
|
||||
val = data.Info.Bandwidth
|
||||
val = data.Info.NetworkSent + data.Info.NetworkRecv
|
||||
unit = " MB/s"
|
||||
case "Disk":
|
||||
maxUsedPct := data.Info.DiskPct
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
beszelTests "beszel/internal/tests"
|
||||
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -1,14 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/agent"
|
||||
"beszel/internal/agent/health"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/agent"
|
||||
"github.com/henrygd/beszel/agent/health"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel/internal/agent"
|
||||
"crypto/ed25519"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/agent"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -1,15 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/hub"
|
||||
_ "beszel/migrations"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/hub"
|
||||
_ "github.com/henrygd/beszel/internal/migrations"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -2,15 +2,15 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
# RUN go mod download
|
||||
COPY *.go ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
COPY ../go.mod ../go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . ./
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
|
||||
@@ -3,16 +3,11 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Download Go modules
|
||||
COPY go.mod go.sum ./
|
||||
COPY ../go.mod ../go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY *.go ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
COPY migrations ./migrations
|
||||
COPY site/dist ./site/dist
|
||||
COPY site/*.go ./site
|
||||
COPY . ./
|
||||
|
||||
RUN apk add --no-cache \
|
||||
unzip \
|
||||
@@ -22,7 +17,7 @@ RUN update-ca-certificates
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./internal/cmd/hub
|
||||
|
||||
# ? -------------------------
|
||||
FROM scratch
|
||||
124
internal/entities/system/system.go
Normal file
124
internal/entities/system/system.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package system
|
||||
|
||||
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
)
|
||||
|
||||
type NetworkInterfaceStats struct {
|
||||
NetworkSent float64 `json:"ns"`
|
||||
NetworkRecv float64 `json:"nr"`
|
||||
MaxNetworkSent float64 `json:"nsm,omitempty"`
|
||||
MaxNetworkRecv float64 `json:"nrm,omitempty"`
|
||||
TotalBytesSent uint64 `json:"tbs,omitempty"` // Total bytes sent since boot
|
||||
TotalBytesRecv uint64 `json:"tbr,omitempty"` // Total bytes received since boot
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
||||
MaxCpu float64 `json:"cpum,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"4,keyasint"`
|
||||
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
|
||||
MemZfsArc float64 `json:"mz,omitempty" cbor:"6,keyasint,omitempty"` // ZFS ARC memory
|
||||
Swap float64 `json:"s,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
SwapUsed float64 `json:"su,omitempty" cbor:"8,keyasint,omitempty"`
|
||||
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
|
||||
DiskReadPs float64 `json:"dr" cbor:"12,keyasint"`
|
||||
DiskWritePs float64 `json:"dw" cbor:"13,keyasint"`
|
||||
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"14,keyasint,omitempty"`
|
||||
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
NetworkInterfaces map[string]NetworkInterfaceStats `json:"ni" cbor:"16,omitempty"` // Per-interface network stats
|
||||
NetworkSent float64 `json:"ns" cbor:"17,keyasint"` // Total network sent (MB/s)
|
||||
NetworkRecv float64 `json:"nr" cbor:"18,keyasint"` // Total network recv (MB/s)
|
||||
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"19,keyasint,omitempty"`
|
||||
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"20,keyasint,omitempty"`
|
||||
Temperatures map[string]float64 `json:"t,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"22,keyasint,omitempty"`
|
||||
GPUData map[string]GPUData `json:"g,omitempty" cbor:"23,keyasint,omitempty"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"24,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"25,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"26,keyasint,omitempty"`
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"27,keyasint"` // [1min, 5min, 15min]
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"28,keyasint,omitzero"` // [percent, charge state]
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"29,keyasint,omitempty"`
|
||||
Nets map[string]float64 `json:"nets,omitempty" cbor:"30,keyasint,omitempty"` // Network connection statistics
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Temperature float64 `json:"-"`
|
||||
MemoryUsed float64 `json:"mu,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
MemoryTotal float64 `json:"mt,omitempty" cbor:"2,keyasint,omitempty"`
|
||||
Usage float64 `json:"u" cbor:"3,keyasint"`
|
||||
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
Count float64 `json:"-"`
|
||||
}
|
||||
|
||||
type FsStats struct {
|
||||
Time time.Time `json:"-"`
|
||||
Root bool `json:"-"`
|
||||
Mountpoint string `json:"-"`
|
||||
DiskTotal float64 `json:"d" cbor:"0,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"1,keyasint"`
|
||||
TotalRead uint64 `json:"-"`
|
||||
TotalWrite uint64 `json:"-"`
|
||||
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
|
||||
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
|
||||
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"5,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type NetIoStats struct {
|
||||
BytesRecv uint64
|
||||
BytesSent uint64
|
||||
PacketsSent uint64
|
||||
PacketsRecv uint64
|
||||
Time time.Time
|
||||
Name string
|
||||
}
|
||||
|
||||
type Os = uint8
|
||||
|
||||
const (
|
||||
Linux Os = iota
|
||||
Darwin
|
||||
Windows
|
||||
Freebsd
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Hostname string `json:"h" cbor:"0,keyasint"`
|
||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
Cores int `json:"c" cbor:"2,keyasint"`
|
||||
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
CpuModel string `json:"m" cbor:"4,keyasint"`
|
||||
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||
NetworkSent float64 `json:"ns" cbor:"9,keyasint"` // Per-interface total (MB/s)
|
||||
NetworkRecv float64 `json:"nr" cbor:"10,keyasint"` // Per-interface total (MB/s)
|
||||
AgentVersion string `json:"v" cbor:"11,keyasint"`
|
||||
Podman bool `json:"p,omitempty" cbor:"12,keyasint,omitempty"`
|
||||
GpuPct float64 `json:"g,omitempty" cbor:"13,keyasint,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"14,keyasint,omitempty"`
|
||||
Os Os `json:"os" cbor:"15,keyasint"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"16,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"17,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"18,keyasint,omitempty"`
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"` // [1min, 5min, 15min]
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
type CombinedData struct {
|
||||
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
||||
Info Info `json:"info" cbor:"1,keyasint"`
|
||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
package ghupdate
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -16,6 +15,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/blang/semver"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/hub/expirymap"
|
||||
"beszel/internal/hub/ws"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -11,6 +8,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/hub/expirymap"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/lxzan/gws"
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -4,9 +4,6 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel/internal/agent"
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/hub/ws"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -17,6 +14,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/agent"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
pbtests "github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -2,13 +2,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/spf13/cast"
|
||||
@@ -4,12 +4,14 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"beszel/internal/hub/config"
|
||||
"beszel/internal/tests"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/henrygd/beszel/internal/hub/config"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -2,12 +2,6 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/alerts"
|
||||
"beszel/internal/hub/config"
|
||||
"beszel/internal/hub/systems"
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/users"
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
@@ -18,6 +12,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/alerts"
|
||||
"github.com/henrygd/beszel/internal/hub/config"
|
||||
"github.com/henrygd/beszel/internal/hub/systems"
|
||||
"github.com/henrygd/beszel/internal/records"
|
||||
"github.com/henrygd/beszel/internal/users"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
@@ -68,6 +69,8 @@ func (h *Hub) StartHub() error {
|
||||
if err := config.SyncSystems(e); err != nil {
|
||||
return err
|
||||
}
|
||||
// register middlewares
|
||||
h.registerMiddlewares(e)
|
||||
// register api routes
|
||||
if err := h.registerApiRoutes(e); err != nil {
|
||||
return err
|
||||
@@ -112,8 +115,6 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
|
||||
// set URL if BASE_URL env is set
|
||||
if h.appURL != "" {
|
||||
settings.Meta.AppURL = h.appURL
|
||||
} else {
|
||||
h.appURL = settings.Meta.AppURL
|
||||
}
|
||||
if err := e.App.Save(settings); err != nil {
|
||||
return err
|
||||
@@ -172,6 +173,37 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// custom middlewares
|
||||
func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
||||
// authorizes request with user matching the provided email
|
||||
authorizeRequestWithEmail := func(e *core.RequestEvent, email string) (err error) {
|
||||
if e.Auth != nil || email == "" {
|
||||
return e.Next()
|
||||
}
|
||||
isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost
|
||||
e.Auth, err = e.App.FindFirstRecordByData("users", "email", email)
|
||||
if err != nil || !isAuthRefresh {
|
||||
return e.Next()
|
||||
}
|
||||
// auth refresh endpoint, make sure token is set in header
|
||||
token, _ := e.Auth.NewAuthToken()
|
||||
e.Request.Header.Set("Authorization", token)
|
||||
return e.Next()
|
||||
}
|
||||
// authenticate with trusted header
|
||||
if autoLogin, _ := GetEnv("AUTO_LOGIN"); autoLogin != "" {
|
||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||
return authorizeRequestWithEmail(e, autoLogin)
|
||||
})
|
||||
}
|
||||
// authenticate with trusted header
|
||||
if trustedHeader, _ := GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||
return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// custom api routes
|
||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
// auth protected routes
|
||||
@@ -4,10 +4,6 @@
|
||||
package hub_test
|
||||
|
||||
import (
|
||||
beszelTests "beszel/internal/tests"
|
||||
"beszel/migrations"
|
||||
"testing"
|
||||
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
@@ -17,6 +13,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/migrations"
|
||||
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
pbTests "github.com/pocketbase/pocketbase/tests"
|
||||
@@ -711,3 +711,117 @@ func TestCreateUserEndpointAvailability(t *testing.T) {
|
||||
scenario.Test(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAutoLoginMiddleware(t *testing.T) {
|
||||
var hubs []*beszelTests.TestHub
|
||||
|
||||
defer func() {
|
||||
defer os.Unsetenv("AUTO_LOGIN")
|
||||
for _, hub := range hubs {
|
||||
hub.Cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
os.Setenv("AUTO_LOGIN", "user@test.com")
|
||||
|
||||
testAppFactory := func(t testing.TB) *pbTests.TestApp {
|
||||
hub, _ := beszelTests.NewTestHub(t.TempDir())
|
||||
hubs = append(hubs, hub)
|
||||
hub.StartHub()
|
||||
return hub.TestApp
|
||||
}
|
||||
|
||||
scenarios := []beszelTests.ApiScenario{
|
||||
{
|
||||
Name: "GET /getkey - without auto login should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /getkey - with auto login should fail if no matching user",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /getkey - with auto login should succeed",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||
TestAppFactory: testAppFactory,
|
||||
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||
beszelTests.CreateUser(app, "user@test.com", "password123")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedHeaderMiddleware(t *testing.T) {
|
||||
var hubs []*beszelTests.TestHub
|
||||
|
||||
defer func() {
|
||||
defer os.Unsetenv("TRUSTED_AUTH_HEADER")
|
||||
for _, hub := range hubs {
|
||||
hub.Cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
os.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted")
|
||||
|
||||
testAppFactory := func(t testing.TB) *pbTests.TestApp {
|
||||
hub, _ := beszelTests.NewTestHub(t.TempDir())
|
||||
hubs = append(hubs, hub)
|
||||
hub.StartHub()
|
||||
return hub.TestApp
|
||||
}
|
||||
|
||||
scenarios := []beszelTests.ApiScenario{
|
||||
{
|
||||
Name: "GET /getkey - without trusted header should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /getkey - with trusted header should fail if no matching user",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
Headers: map[string]string{
|
||||
"X-Beszel-Trusted": "user@test.com",
|
||||
},
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /getkey - with trusted header should succeed",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
Headers: map[string]string{
|
||||
"X-Beszel-Trusted": "user@test.com",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"\"key\":", "\"v\":"},
|
||||
TestAppFactory: testAppFactory,
|
||||
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||
beszelTests.CreateUser(app, "user@test.com", "password123")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package hub
|
||||
|
||||
import "beszel/internal/hub/systems"
|
||||
import "github.com/henrygd/beszel/internal/hub/systems"
|
||||
|
||||
// TESTING ONLY: GetSystemManager returns the system manager
|
||||
func (h *Hub) GetSystemManager() *systems.SystemManager {
|
||||
@@ -3,7 +3,6 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -12,7 +11,10 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
)
|
||||
|
||||
// Wraps http.RoundTripper to modify dev proxy HTML responses
|
||||
@@ -75,5 +77,6 @@ func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||
proxy.ServeHTTP(e.Response, e.Request)
|
||||
return nil
|
||||
})
|
||||
_ = osutils.LaunchURL(h.appURL)
|
||||
return nil
|
||||
}
|
||||
@@ -3,13 +3,14 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/site"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/site"
|
||||
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
@@ -1,9 +1,6 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/ws"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -13,6 +10,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -1,14 +1,18 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/ws"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
@@ -30,10 +34,8 @@ const (
|
||||
sessionTimeout = 4 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
// errSystemExists is returned when attempting to add a system that already exists
|
||||
errSystemExists = errors.New("system exists")
|
||||
)
|
||||
// errSystemExists is returned when attempting to add a system that already exists
|
||||
var errSystemExists = errors.New("system exists")
|
||||
|
||||
// SystemManager manages a collection of monitored systems and their connections.
|
||||
// It handles system lifecycle, status updates, and maintains both SSH and WebSocket connections.
|
||||
@@ -4,16 +4,17 @@
|
||||
package systems_test
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/systems"
|
||||
"beszel/internal/tests"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/hub/systems"
|
||||
"github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -4,9 +4,10 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
entities "beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
entities "github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
// TESTING ONLY: GetSystemCount returns the number of systems in the store
|
||||
@@ -1,12 +1,12 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel/internal/ghupdate"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/henrygd/beszel/internal/ghupdate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/entities/system"
|
||||
"errors"
|
||||
"time"
|
||||
"weak"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/lxzan/gws"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -4,11 +4,12 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"beszel/internal/common"
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -2,8 +2,6 @@
|
||||
package records
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -11,6 +9,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
@@ -39,12 +40,14 @@ type StatsRecord struct {
|
||||
}
|
||||
|
||||
// global variables for reusing allocations
|
||||
var statsRecord StatsRecord
|
||||
var containerStats []container.Stats
|
||||
var sumStats system.Stats
|
||||
var tempStats system.Stats
|
||||
var queryParams = make(dbx.Params, 1)
|
||||
var containerSums = make(map[string]*container.Stats)
|
||||
var (
|
||||
statsRecord StatsRecord
|
||||
containerStats []container.Stats
|
||||
sumStats system.Stats
|
||||
tempStats system.Stats
|
||||
queryParams = make(dbx.Params, 1)
|
||||
containerSums = make(map[string]*container.Stats)
|
||||
)
|
||||
|
||||
// Create longer records by averaging shorter records
|
||||
func (rm *RecordManager) CreateLongerRecords() {
|
||||
@@ -203,15 +206,51 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.DiskPct += stats.DiskPct
|
||||
sum.DiskReadPs += stats.DiskReadPs
|
||||
sum.DiskWritePs += stats.DiskWritePs
|
||||
sum.LoadAvg1 += stats.LoadAvg1
|
||||
sum.LoadAvg5 += stats.LoadAvg5
|
||||
sum.LoadAvg15 += stats.LoadAvg15
|
||||
sum.NetworkSent += stats.NetworkSent
|
||||
sum.NetworkRecv += stats.NetworkRecv
|
||||
sum.LoadAvg[0] += stats.LoadAvg[0]
|
||||
sum.LoadAvg[1] += stats.LoadAvg[1]
|
||||
sum.LoadAvg[2] += stats.LoadAvg[2]
|
||||
sum.Bandwidth[0] += stats.Bandwidth[0]
|
||||
sum.Bandwidth[1] += stats.Bandwidth[1]
|
||||
batterySum += int(stats.Battery[0])
|
||||
sum.Battery[1] = stats.Battery[1]
|
||||
|
||||
if stats.NetworkInterfaces != nil {
|
||||
if sum.NetworkInterfaces == nil {
|
||||
sum.NetworkInterfaces = make(map[string]system.NetworkInterfaceStats, len(stats.NetworkInterfaces))
|
||||
}
|
||||
for key, value := range stats.NetworkInterfaces {
|
||||
if _, ok := sum.NetworkInterfaces[key]; !ok {
|
||||
sum.NetworkInterfaces[key] = system.NetworkInterfaceStats{}
|
||||
}
|
||||
ni := sum.NetworkInterfaces[key]
|
||||
ni.NetworkSent += value.NetworkSent
|
||||
ni.NetworkRecv += value.NetworkRecv
|
||||
ni.MaxNetworkSent += value.MaxNetworkSent
|
||||
ni.MaxNetworkRecv += value.MaxNetworkRecv
|
||||
// For cumulative totals, use the maximum value (most recent)
|
||||
if value.TotalBytesSent > ni.TotalBytesSent {
|
||||
ni.TotalBytesSent = value.TotalBytesSent
|
||||
}
|
||||
if value.TotalBytesRecv > ni.TotalBytesRecv {
|
||||
ni.TotalBytesRecv = value.TotalBytesRecv
|
||||
}
|
||||
sum.NetworkInterfaces[key] = ni
|
||||
}
|
||||
}
|
||||
|
||||
// Handle network connection stats - use the latest values (most recent sample)
|
||||
if stats.Nets != nil {
|
||||
if sum.Nets == nil {
|
||||
sum.Nets = make(map[string]float64)
|
||||
}
|
||||
for key, value := range stats.Nets {
|
||||
sum.Nets[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Set peak values
|
||||
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
|
||||
@@ -219,8 +258,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
|
||||
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
|
||||
sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)
|
||||
sum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0])
|
||||
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
|
||||
|
||||
// Accumulate temperatures
|
||||
if stats.Temperatures != nil {
|
||||
@@ -288,14 +325,26 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.DiskPct = twoDecimals(sum.DiskPct / count)
|
||||
sum.DiskReadPs = twoDecimals(sum.DiskReadPs / count)
|
||||
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
|
||||
sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count)
|
||||
sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count)
|
||||
sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count)
|
||||
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
|
||||
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
||||
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
|
||||
sum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count)
|
||||
sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
|
||||
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
|
||||
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
|
||||
sum.Battery[0] = uint8(batterySum / int(count))
|
||||
|
||||
if sum.NetworkInterfaces != nil {
|
||||
for key := range sum.NetworkInterfaces {
|
||||
ni := sum.NetworkInterfaces[key]
|
||||
ni.NetworkSent = twoDecimals(ni.NetworkSent / count)
|
||||
ni.NetworkRecv = twoDecimals(ni.NetworkRecv / count)
|
||||
ni.MaxNetworkSent = twoDecimals(max(ni.MaxNetworkSent, ni.NetworkSent))
|
||||
ni.MaxNetworkRecv = twoDecimals(max(ni.MaxNetworkRecv, ni.NetworkRecv))
|
||||
sum.NetworkInterfaces[key] = ni
|
||||
}
|
||||
}
|
||||
// Average temperatures
|
||||
if sum.Temperatures != nil && tempCount > 0 {
|
||||
for key := range sum.Temperatures {
|
||||
@@ -360,19 +409,15 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
|
||||
}
|
||||
sums[stat.Name].Cpu += stat.Cpu
|
||||
sums[stat.Name].Mem += stat.Mem
|
||||
sums[stat.Name].NetworkSent += stat.NetworkSent
|
||||
sums[stat.Name].NetworkRecv += stat.NetworkRecv
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]container.Stats, 0, len(sums))
|
||||
for _, value := range sums {
|
||||
result = append(result, container.Stats{
|
||||
Name: value.Name,
|
||||
Cpu: twoDecimals(value.Cpu / count),
|
||||
Mem: twoDecimals(value.Mem / count),
|
||||
NetworkSent: twoDecimals(value.NetworkSent / count),
|
||||
NetworkRecv: twoDecimals(value.NetworkRecv / count),
|
||||
Name: value.Name,
|
||||
Cpu: twoDecimals(value.Cpu / count),
|
||||
Mem: twoDecimals(value.Mem / count),
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -4,12 +4,13 @@
|
||||
package records_test
|
||||
|
||||
import (
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/tests"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/records"
|
||||
"github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
41
internal/site/biome.json
Normal file
41
internal/site/biome.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 120
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useUniqueElementIds": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "asNeeded",
|
||||
"trailingCommas": "es5"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
internal/site/bun.lockb
Executable file
BIN
internal/site/bun.lockb
Executable file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,11 @@
|
||||
"build": "lingui extract --overwrite && lingui compile && vite build",
|
||||
"preview": "vite preview",
|
||||
"sync": "lingui extract --overwrite && lingui compile",
|
||||
"sync_and_purge": "lingui extract --overwrite --clean && lingui compile"
|
||||
"sync_and_purge": "lingui extract --overwrite --clean && lingui compile",
|
||||
"format": "biome format --write .",
|
||||
"lint": "biome lint .",
|
||||
"check": "biome check .",
|
||||
"check:fix": "biome check --fix ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@henrygd/queue": "^1.0.7",
|
||||
@@ -38,6 +42,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"d3-time": "^3.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanostores": "^0.11.4",
|
||||
"pocketbase": "^0.26.2",
|
||||
@@ -48,6 +53,7 @@
|
||||
"valibot": "^0.42.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.3",
|
||||
"@lingui/cli": "^5.4.1",
|
||||
"@lingui/swc-plugin": "^5.6.1",
|
||||
"@lingui/vite-plugin": "^5.4.1",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user