Compare commits

..

28 Commits

Author SHA1 Message Date
hank
9fcf7f1fff New translations en.po (Persian) 2025-08-25 17:21:19 -04:00
hank
d909f0a997 New translations en.po (Icelandic) 2025-08-25 17:21:18 -04:00
hank
c53c8f3cdd New translations en.po (Ukrainian) 2025-08-25 17:21:17 -04:00
hank
fedbef3b6d New translations en.po (Turkish) 2025-08-25 17:21:16 -04:00
hank
16004c72c9 New translations en.po (Swedish) 2025-08-25 17:21:15 -04:00
hank
acd2200f72 New translations en.po (Slovenian) 2025-08-25 17:21:14 -04:00
hank
44e6d78831 New translations en.po (Norwegian) 2025-08-25 17:21:13 -04:00
hank
a4d81d3849 New translations en.po (Hungarian) 2025-08-25 17:21:12 -04:00
hank
139af4ab02 New translations en.po (Greek) 2025-08-25 17:21:11 -04:00
hank
67b19086fa New translations en.po (Spanish) 2025-08-25 17:21:10 -04:00
hank
809ce1c2b7 New translations en.po (French) 2025-08-25 17:21:09 -04:00
hank
041483b643 New translations en.po (Russian) 2025-08-25 17:21:08 -04:00
hank
560d118129 New translations en.po (Chinese Traditional, Hong Kong) 2025-08-25 17:21:07 -04:00
hank
e528c5ec64 New translations en.po (Croatian) 2025-08-25 17:21:06 -04:00
hank
cbacb7bdc8 New translations en.po (Chinese Traditional) 2025-08-25 17:21:05 -04:00
hank
55ab322039 New translations en.po (Chinese Simplified) 2025-08-25 17:21:04 -04:00
hank
f80e76041a New translations en.po (Portuguese) 2025-08-25 17:21:03 -04:00
hank
1ca1a0042e New translations en.po (Dutch) 2025-08-25 17:21:02 -04:00
hank
9314b380f2 New translations en.po (Korean) 2025-08-25 17:21:01 -04:00
hank
67cbe547a8 New translations en.po (Italian) 2025-08-25 17:21:00 -04:00
hank
30721d7dea New translations en.po (Danish) 2025-08-25 17:20:59 -04:00
hank
fdcfc9f447 New translations en.po (Czech) 2025-08-25 17:20:58 -04:00
hank
dac990e181 New translations en.po (Bulgarian) 2025-08-25 17:20:57 -04:00
hank
b32139bac9 New translations en.po (Arabic) 2025-08-25 17:20:56 -04:00
hank
40a5e45da7 New translations en.po (German) 2025-08-25 17:20:55 -04:00
hank
9ed7dc5ee8 New translations en.po (Vietnamese) 2025-08-25 17:20:54 -04:00
hank
a88d5651d4 New translations en.po (Polish) 2025-08-25 17:20:53 -04:00
hank
10c7946c8f New translations en.po (Japanese) 2025-08-25 17:20:51 -04:00
118 changed files with 2550 additions and 3666 deletions

View File

@@ -93,9 +93,7 @@ jobs:
# https://github.com/docker/login-action
- name: Login to Docker Hub
env:
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ matrix.username || secrets[matrix.username_secret] }}
@@ -110,6 +108,6 @@ jobs:
context: "${{ matrix.context }}"
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

View File

@@ -51,4 +51,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
IS_FORK: ${{ github.repository_owner != 'henrygd' }}

View File

@@ -15,7 +15,7 @@ permissions:
jobs:
vulncheck:
name: VulnCheck
name: Analysis
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
@@ -23,8 +23,8 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.25.x
# cached: false
go-version: 1.24.x
cached: false
- name: Get official govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
shell: bash

View File

@@ -38,25 +38,12 @@ builds:
- mips64
- riscv64
- mipsle
- mips
- ppc64le
gomips:
- hardfloat
- softfloat
ignore:
- goos: freebsd
goarch: arm
- goos: openbsd
goarch: arm
- goos: linux
goarch: mips64
gomips: softfloat
- goos: linux
goarch: mipsle
gomips: hardfloat
- goos: linux
goarch: mips
gomips: hardfloat
- goos: windows
goarch: arm
- goos: darwin
@@ -67,7 +54,7 @@ builds:
archives:
- id: beszel-agent
formats: [tar.gz]
ids:
builds:
- beszel-agent
name_template: >-
{{ .Binary }}_
@@ -79,7 +66,7 @@ archives:
- id: beszel
formats: [tar.gz]
ids:
builds:
- beszel
name_template: >-
{{ .Binary }}_
@@ -98,7 +85,7 @@ nfpms:
API access.
maintainer: henrygd <hank@henrygd.me>
section: net
ids:
builds:
- beszel-agent
formats:
- deb
@@ -135,7 +122,6 @@ scoops:
homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
# chocolateys:
@@ -169,7 +155,7 @@ brews:
homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
skip_upload: auto
extra_install: |
(bin/"beszel-agent-launcher").write <<~EOS
#!/bin/bash
@@ -201,7 +187,7 @@ winget:
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
publisher_support_url: "https://github.com/henrygd/beszel/issues"
short_description: "Agent for Beszel, a lightweight server monitoring platform."
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
skip_upload: auto
description: |
Beszel is a lightweight server monitoring platform that includes Docker
statistics, historical data, and alert functions. It has a friendly web

View File

@@ -7,7 +7,7 @@ SKIP_WEB ?= false
# Set executable extension based on target OS
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
.PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales
.DEFAULT_GOAL := build
clean:
@@ -53,10 +53,6 @@ build-agent: tidy build-dotnet-conditional
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
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
build: build-agent build-hub
generate-locales:
@@ -77,9 +73,9 @@ dev-hub: export ENV=dev
dev-hub:
mkdir -p ./site/dist && touch ./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 ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \
else \
cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
fi
dev-agent:

View File

@@ -4,12 +4,12 @@ import (
"beszel"
"beszel/internal/agent"
"beszel/internal/agent/health"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh"
)
@@ -17,24 +17,43 @@ import (
type cmdOptions struct {
key string // key is the public key(s) for SSH authentication.
listen string // listen is the address or port to listen on.
// TODO: add hubURL and token
// hubURL string // hubURL is the URL of the hub to use.
// token string // token is the token to use for authentication.
}
// parse parses the command line flags and populates the config struct.
// It returns true if a subcommand was handled and the program should exit.
func (opts *cmdOptions) parse() bool {
flag.StringVar(&opts.key, "key", "", "Public key(s) for SSH authentication")
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
flag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
flag.PrintDefaults()
}
subcommand := ""
if len(os.Args) > 1 {
subcommand = os.Args[1]
}
// Subcommands that don't require any pflag parsing
switch subcommand {
case "-v", "version":
fmt.Println(beszel.AppName+"-agent", beszel.Version)
return true
case "help":
flag.Usage()
return true
case "update":
agent.Update()
return true
case "health":
err := health.Check()
if err != nil {
@@ -44,57 +63,7 @@ func (opts *cmdOptions) parse() bool {
return true
}
// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication")
pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on")
// pflag.StringVarP(&opts.hubURL, "hub-url", "u", "", "URL of the hub to use")
// pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
help := pflag.BoolP("help", "h", false, "Show this help message")
// Convert old single-dash long flags to double-dash for backward compatibility
flagsToConvert := []string{"key", "listen"}
for i, arg := range os.Args {
for _, flag := range flagsToConvert {
singleDash := "-" + flag
doubleDash := "--" + flag
if arg == singleDash {
os.Args[i] = doubleDash
break
} else if strings.HasPrefix(arg, singleDash+"=") {
os.Args[i] = doubleDash + arg[len(singleDash):]
break
}
}
}
pflag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
// builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
pflag.PrintDefaults()
}
// Parse all arguments with pflag
pflag.Parse()
// Must run after pflag.Parse()
switch {
case *help || subcommand == "help":
pflag.Usage()
return true
case subcommand == "update":
agent.Update(*chinaMirrors)
return true
}
flag.Parse()
return false
}

View File

@@ -3,11 +3,11 @@ package main
import (
"beszel/internal/agent"
"crypto/ed25519"
"flag"
"os"
"path/filepath"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
@@ -245,7 +245,7 @@ func TestParseFlags(t *testing.T) {
oldArgs := os.Args
defer func() {
os.Args = oldArgs
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
}()
tests := []struct {
@@ -269,22 +269,6 @@ func TestParseFlags(t *testing.T) {
listen: "",
},
},
{
name: "key flag double dash",
args: []string{"cmd", "--key", "testkey"},
expected: cmdOptions{
key: "testkey",
listen: "",
},
},
{
name: "key flag short",
args: []string{"cmd", "-k", "testkey"},
expected: cmdOptions{
key: "testkey",
listen: "",
},
},
{
name: "addr flag only",
args: []string{"cmd", "-listen", ":8080"},
@@ -293,22 +277,6 @@ func TestParseFlags(t *testing.T) {
listen: ":8080",
},
},
{
name: "addr flag double dash",
args: []string{"cmd", "--listen", ":8080"},
expected: cmdOptions{
key: "",
listen: ":8080",
},
},
{
name: "addr flag short",
args: []string{"cmd", "-l", ":8080"},
expected: cmdOptions{
key: "",
listen: ":8080",
},
},
{
name: "both flags",
args: []string{"cmd", "-key", "testkey", "-listen", ":8080"},
@@ -322,12 +290,12 @@ func TestParseFlags(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset flags for each test
pflag.CommandLine = pflag.NewFlagSet(tt.args[0], pflag.ExitOnError)
flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError)
os.Args = tt.args
var opts cmdOptions
opts.parse()
pflag.Parse()
flag.Parse()
assert.Equal(t, tt.expected, opts)
})

View File

@@ -45,13 +45,11 @@ func getBaseApp() *pocketbase.PocketBase {
baseApp.RootCmd.Use = beszel.AppName
baseApp.RootCmd.Short = ""
// add update command
updateCmd := &cobra.Command{
baseApp.RootCmd.AddCommand(&cobra.Command{
Use: "update",
Short: "Update " + beszel.AppName + " to the latest version",
Run: hub.Update,
}
updateCmd.Flags().Bool("china-mirrors", false, "Use mirror (gh.beszel.dev) instead of GitHub")
baseApp.RootCmd.AddCommand(updateCmd)
})
// add health command
baseApp.RootCmd.AddCommand(newHealthCmd())

View File

@@ -15,7 +15,7 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
# --------------------------
# Final image: GPU-enabled agent with nvidia-smi
# --------------------------
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
FROM nvidia/cuda:12.9.1-base-ubuntu22.04
COPY --from=builder /agent /agent
ENTRYPOINT ["/agent"]

View File

@@ -1,6 +1,6 @@
module beszel
go 1.25.1
go 1.24.4
// lock shoutrrr to specific version to allow review before updating
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8
@@ -15,10 +15,10 @@ require (
github.com/nicholas-fedor/shoutrrr v0.8.17
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.29.3
github.com/shirou/gopsutil/v4 v4.25.6
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/shirou/gopsutil/v4 v4.25.7
github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.11.0
golang.org/x/crypto v0.41.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
@@ -41,6 +41,9 @@ require (
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d // indirect
@@ -50,8 +53,11 @@ require (
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/ulikunitz/xz v0.5.13 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/image v0.30.0 // indirect
@@ -60,7 +66,6 @@ require (
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect

View File

@@ -27,6 +27,7 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
@@ -49,13 +50,24 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
@@ -63,8 +75,11 @@ github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLany
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d h1:vFzYZc8yji+9DmNRhpEbs8VBK4CgV/DPfGzeVJSSp/8=
@@ -79,8 +94,11 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.8.8 h1:F/oyoatWK5cbHPPgkjRZrA0262TP7KWuUQz9KskRtR8=
github.com/nicholas-fedor/shoutrrr v0.8.8/go.mod h1:T30Y+eoZFEjDk4HtOItcHQioZSOe3Z6a6aNfSz6jc5c=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -94,11 +112,13 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
@@ -110,10 +130,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.13 h1:ar98gWrjf4H1ev05fYP/o29PDZw9DrI3niHtnEqyuXA=
github.com/ulikunitz/xz v0.5.13/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -121,6 +146,7 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
@@ -130,53 +156,72 @@ golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=

View File

@@ -36,6 +36,7 @@ type Agent struct {
server *ssh.Server // SSH server
dataDir string // Directory for persisting data
keys []gossh.PublicKey // SSH public keys
hasBattery bool // true if agent has access to battery stats
}
// NewAgent creates a new agent with the given data directory for persisting data.

View File

@@ -14,7 +14,7 @@ import (
)
func TestSessionCache_GetSet(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
synctest.Run(func() {
cache := NewSessionCache(69 * time.Second)
testData := &system.CombinedData{

View File

@@ -0,0 +1,24 @@
package agent
import "github.com/distatus/battery"
// getBatteryStats returns the current battery percent and charge state
func getBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
batteries, err := battery.GetAll()
if err != nil {
return batteryPercent, batteryState, err
}
totalCapacity := float64(0)
totalCharge := float64(0)
for _, bat := range batteries {
if bat.Design != 0 {
totalCapacity += bat.Design
} else {
totalCapacity += bat.Full
}
totalCharge += bat.Current
}
batteryPercent = uint8(totalCharge / totalCapacity * 100)
batteryState = uint8(batteries[0].State.Raw)
return batteryPercent, batteryState, nil
}

View File

@@ -1,53 +0,0 @@
//go:build !freebsd
// Package battery provides functions to check if the system has a battery and to get the battery stats.
package battery
import (
"errors"
"log/slog"
"github.com/distatus/battery"
)
var systemHasBattery = false
var haveCheckedBattery = false
// HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool {
if haveCheckedBattery {
return systemHasBattery
}
haveCheckedBattery = true
bat, err := battery.Get(0)
if err == nil && bat != nil {
systemHasBattery = true
} else {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery
}
// GetBatteryStats returns the current battery percent and charge state
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !systemHasBattery {
return batteryPercent, batteryState, errors.ErrUnsupported
}
batteries, err := battery.GetAll()
if err != nil || len(batteries) == 0 {
return batteryPercent, batteryState, err
}
totalCapacity := float64(0)
totalCharge := float64(0)
for _, bat := range batteries {
if bat.Design != 0 {
totalCapacity += bat.Design
} else {
totalCapacity += bat.Full
}
totalCharge += bat.Current
}
batteryPercent = uint8(totalCharge / totalCapacity * 100)
batteryState = uint8(batteries[0].State.Raw)
return batteryPercent, batteryState, nil
}

View File

@@ -1,13 +0,0 @@
//go:build freebsd
package battery
import "errors"
func HasReadableBattery() bool {
return false
}
func GetBatteryStats() (uint8, uint8, error) {
return 0, 0, errors.ErrUnsupported
}

View File

@@ -39,7 +39,7 @@ func TestHealth(t *testing.T) {
// This test uses synctest to simulate time passing.
// NOTE: This test requires GOEXPERIMENT=synctest to run.
t.Run("check with simulated time", func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
synctest.Run(func() {
// Update the file to set the initial timestamp.
require.NoError(t, Update(), "Update() failed inside synctest")

View File

@@ -46,10 +46,9 @@ var lhmFs embed.FS
var (
beszelLhm *lhmProcess
beszelLhmOnce sync.Once
useLHM = os.Getenv("LHM") == "true"
)
var errNoSensors = errors.New("no sensors found (try running as admin with LHM=true)")
var errNoSensors = errors.New("no sensors found (try running as admin)")
// newlhmProcess copies the embedded LHM executable to a temporary directory and starts it.
func newlhmProcess() (*lhmProcess, error) {
@@ -140,7 +139,7 @@ func (lhm *lhmProcess) cleanupProcess() {
}
func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {
if !useLHM || lhm.stoppedNoSensors {
if lhm.stoppedNoSensors {
// Fall back to gopsutil if we can't get sensors from LHM
return sensors.TemperaturesWithContext(ctx)
}
@@ -223,10 +222,6 @@ func getSensorTemps(ctx context.Context) (temps []sensors.TemperatureStat, err e
}
}()
if !useLHM {
return sensors.TemperaturesWithContext(ctx)
}
// Initialize process once
beszelLhmOnce.Do(func() {
beszelLhm, err = newlhmProcess()

View File

@@ -2,7 +2,6 @@ package agent
import (
"beszel"
"beszel/internal/agent/battery"
"beszel/internal/entities/system"
"bufio"
"fmt"
@@ -65,6 +64,13 @@ func (a *Agent) initializeSystemInfo() {
} else {
a.zfs = true
}
// battery
if _, _, err := getBatteryStats(); err != nil {
slog.Debug("No battery detected", "err", err)
} else {
a.hasBattery = true
}
}
// Returns current info, stats about the host system
@@ -72,8 +78,8 @@ func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{}
// battery
if battery.HasReadableBattery() {
systemStats.Battery[0], systemStats.Battery[1], _ = battery.GetBatteryStats()
if a.hasBattery {
systemStats.Battery[0], systemStats.Battery[1], _ = getBatteryStats()
}
// cpu percent

View File

@@ -1,150 +1,56 @@
package agent
import (
"beszel/internal/ghupdate"
"beszel"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
)
// restarter knows how to restart the beszel-agent service.
type restarter interface {
Restart() error
}
type systemdRestarter struct{ cmd string }
func (s *systemdRestarter) Restart() error {
// Only restart if the service is active
if err := exec.Command(s.cmd, "is-active", "beszel-agent.service").Run(); err != nil {
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent.service via systemd…")
return exec.Command(s.cmd, "restart", "beszel-agent.service").Run()
}
type openRCRestarter struct{ cmd string }
func (o *openRCRestarter) Restart() error {
if err := exec.Command(o.cmd, "status", "beszel-agent").Run(); err != nil {
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via OpenRC…")
return exec.Command(o.cmd, "restart", "beszel-agent").Run()
}
type openWRTRestarter struct{ cmd string }
func (w *openWRTRestarter) Restart() error {
if err := exec.Command(w.cmd, "running", "beszel-agent").Run(); err != nil {
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via procd…")
return exec.Command(w.cmd, "restart", "beszel-agent").Run()
}
func detectRestarter() restarter {
if path, err := exec.LookPath("systemctl"); err == nil {
return &systemdRestarter{cmd: path}
}
if path, err := exec.LookPath("rc-service"); err == nil {
return &openRCRestarter{cmd: path}
}
if path, err := exec.LookPath("service"); err == nil {
return &openWRTRestarter{cmd: path}
}
return nil
}
// Update checks GitHub for a newer release of beszel-agent, applies it,
// fixes SELinux context if needed, and restarts the service.
func Update(useMirror bool) error {
exePath, _ := os.Executable()
dataDir, err := getDataDir()
if err != nil {
dataDir = os.TempDir()
}
updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel-agent",
DataDir: dataDir,
UseMirror: useMirror,
// Update updates beszel-agent to the latest version
func Update() {
var latest *selfupdate.Release
var found bool
var err error
currentVersion := semver.MustParse(beszel.Version)
fmt.Println("beszel-agent", currentVersion)
fmt.Println("Checking for updates...")
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"beszel-agent"},
})
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil {
log.Fatal(err)
}
if !updated {
return nil
fmt.Println("Error checking for updates:", err)
os.Exit(1)
}
// make sure the file is executable
if err := os.Chmod(exePath, 0755); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to set executable permissions: %v", err)
}
// set ownership to beszel:beszel if possible
if chownPath, err := exec.LookPath("chown"); err == nil {
if err := exec.Command(chownPath, "beszel:beszel", exePath).Run(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to set file ownership: %v", err)
}
if !found {
fmt.Println("No updates found")
os.Exit(0)
}
// 6) Fix SELinux context if necessary
if err := handleSELinuxContext(exePath); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: SELinux context handling: %v", err)
fmt.Println("Latest version:", latest.Version)
if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return
}
// 7) Restart service if running under a recognised init system
if r := detectRestarter(); r != nil {
if err := r.Restart(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to restart service: %v", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually.")
} else {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
} else {
ghupdate.ColorPrint(ghupdate.ColorYellow, "No supported init system detected; please restart manually if needed.")
}
return nil
}
// handleSELinuxContext restores or applies the correct SELinux label to the binary.
func handleSELinuxContext(path string) error {
out, err := exec.Command("getenforce").Output()
var binaryPath string
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
binaryPath, err = os.Executable()
if err != nil {
// SELinux not enabled or getenforce not available
return nil
fmt.Println("Error getting binary path:", err)
os.Exit(1)
}
state := strings.TrimSpace(string(out))
if state == "Disabled" {
return nil
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
if err != nil {
fmt.Println("Please try rerunning with sudo. Error:", err)
os.Exit(1)
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "SELinux is enabled; applying context…")
var errs []string
// Try persistent context via semanage+restorecon
if semanagePath, err := exec.LookPath("semanage"); err == nil {
if err := exec.Command(semanagePath, "fcontext", "-a", "-t", "bin_t", path).Run(); err != nil {
errs = append(errs, "semanage fcontext failed: "+err.Error())
} else if restoreconPath, err := exec.LookPath("restorecon"); err == nil {
if err := exec.Command(restoreconPath, "-v", path).Run(); err != nil {
errs = append(errs, "restorecon failed: "+err.Error())
}
}
}
// Fallback to temporary context via chcon
if chconPath, err := exec.LookPath("chcon"); err == nil {
if err := exec.Command(chconPath, "-t", "bin_t", path).Run(); err != nil {
errs = append(errs, "chcon failed: "+err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("SELinux context errors: %s", strings.Join(errs, "; "))
}
return nil
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
}

View File

@@ -87,7 +87,7 @@ var supportsTitle = map[string]struct{}{
func NewAlertManager(app hubLike) *AlertManager {
am := &AlertManager{
hub: app,
alertQueue: make(chan alertTask, 5),
alertQueue: make(chan alertTask),
stopChan: make(chan struct{}),
}
am.bindEvents()

View File

@@ -42,10 +42,21 @@ func updateHistoryOnAlertUpdate(e *core.RecordEvent) error {
// resolveAlertHistoryRecord sets the resolved field to the current time
func resolveAlertHistoryRecord(app core.App, alertRecordID string) error {
alertHistoryRecord, err := app.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id} && resolved=null", dbx.Params{"alert_id": alertRecordID})
if err != nil || alertHistoryRecord == nil {
alertHistoryRecords, err := app.FindRecordsByFilter(
"alerts_history",
"alert_id={:alert_id} && resolved=null",
"-created",
1,
0,
dbx.Params{"alert_id": alertRecordID},
)
if err != nil {
return err
}
if len(alertHistoryRecords) == 0 {
return nil
}
alertHistoryRecord := alertHistoryRecords[0] // there should be only one record
alertHistoryRecord.Set("resolved", time.Now().UTC())
err = app.Save(alertHistoryRecord)
if err != nil {

View File

@@ -10,8 +10,6 @@ import (
"net/http"
"strings"
"testing"
"testing/synctest"
"time"
beszelTests "beszel/internal/tests"
@@ -65,14 +63,14 @@ func TestUserAlertsApi(t *testing.T) {
}
scenarios := []beszelTests.ApiScenario{
// {
// Name: "GET not implemented - returns index",
// Method: http.MethodGet,
// URL: "/api/beszel/user-alerts",
// ExpectedStatus: 200,
// ExpectedContent: []string{"<html ", "globalThis.BESZEL"},
// TestAppFactory: testAppFactory,
// },
{
Name: "GET not implemented - returns index",
Method: http.MethodGet,
URL: "/api/beszel/user-alerts",
ExpectedStatus: 200,
ExpectedContent: []string{"<html ", "globalThis.BESZEL"},
TestAppFactory: testAppFactory,
},
{
Name: "POST no auth",
Method: http.MethodPost,
@@ -368,237 +366,3 @@ func TestUserAlertsApi(t *testing.T) {
scenario.Test(t)
}
}
func getHubWithUser(t *testing.T) (*beszelTests.TestHub, *core.Record) {
hub, err := beszelTests.NewTestHub(t.TempDir())
assert.NoError(t, err)
hub.StartHub()
// Manually initialize the system manager to bind event hooks
err = hub.GetSystemManager().Initialize()
assert.NoError(t, err)
// Create a test user
user, err := beszelTests.CreateUser(hub, "test@example.com", "password")
assert.NoError(t, err)
// Create user settings for the test user (required for alert notifications)
userSettingsData := map[string]any{
"user": user.Id,
"settings": `{"emails":[test@example.com],"webhooks":[]}`,
}
_, err = beszelTests.CreateRecord(hub, "user_settings", userSettingsData)
assert.NoError(t, err)
return hub, user
}
func TestStatusAlerts(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
defer hub.Cleanup()
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
assert.NoError(t, err)
var alerts []*core.Record
for i, system := range systems {
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": i + 1,
})
assert.NoError(t, err)
alerts = append(alerts, alert)
}
time.Sleep(10 * time.Millisecond)
for _, alert := range alerts {
assert.False(t, alert.GetBool("triggered"), "Alert should not be triggered immediately")
}
if hub.TestMailer.TotalSend() != 0 {
assert.Zero(t, hub.TestMailer.TotalSend(), "Expected 0 messages, got %d", hub.TestMailer.TotalSend())
}
for _, system := range systems {
assert.EqualValues(t, "paused", system.GetString("status"), "System should be paused")
}
for _, system := range systems {
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
time.Sleep(time.Second)
assert.EqualValues(t, 0, hub.GetPendingAlertsCount(), "should have 0 alerts in the pendingAlerts map")
for _, system := range systems {
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
// after 30 seconds, should have 4 alerts in the pendingAlerts map, no triggered alerts
time.Sleep(time.Second * 30)
assert.EqualValues(t, 4, hub.GetPendingAlertsCount(), "should have 4 alerts in the pendingAlerts map")
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 0, triggeredCount, "should have 0 alert triggered")
assert.EqualValues(t, 0, hub.TestMailer.TotalSend(), "should have 0 messages sent")
// after 1:30 seconds, should have 1 triggered alert and 3 pending alerts
time.Sleep(time.Second * 60)
assert.EqualValues(t, 3, hub.GetPendingAlertsCount(), "should have 3 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "should have 1 alert triggered")
assert.EqualValues(t, 1, hub.TestMailer.TotalSend(), "should have 1 messages sent")
// after 2:30 seconds, should have 2 triggered alerts and 2 pending alerts
time.Sleep(time.Second * 60)
assert.EqualValues(t, 2, hub.GetPendingAlertsCount(), "should have 2 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 2, triggeredCount, "should have 2 alert triggered")
assert.EqualValues(t, 2, hub.TestMailer.TotalSend(), "should have 2 messages sent")
// now we will bring the remaning systems back up
for _, system := range systems {
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
time.Sleep(time.Second)
// should have 0 alerts in the pendingAlerts map and 0 alerts triggered
assert.EqualValues(t, 0, hub.GetPendingAlertsCount(), "should have 0 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.Zero(t, triggeredCount, "should have 0 alert triggered")
// 4 messages sent, 2 down alerts and 2 up alerts for first 2 systems
assert.EqualValues(t, 4, hub.TestMailer.TotalSend(), "should have 4 messages sent")
})
}
func TestAlertsHistory(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
defer hub.Cleanup()
// Create systems and alerts
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Initially, no alert history records should exist
initialHistoryCount, err := hub.CountRecords("alerts_history", nil)
assert.NoError(t, err)
assert.Zero(t, initialHistoryCount, "Should have 0 alert history records initially")
// Set system to up initially
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(10 * time.Millisecond)
// Set system to down to trigger alert
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
// Wait for alert to trigger (after the downtime delay)
// With 1 minute delay, we need to wait at least 1 minute + some buffer
time.Sleep(time.Second * 75)
// Check that alert is triggered
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "Alert should be triggered")
// Check that alert history record was created
historyCount, err := hub.CountRecords("alerts_history", dbx.HashExp{"alert_id": alert.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, historyCount, "Should have 1 alert history record for triggered alert")
// Get the alert history record and verify it's not resolved immediately
historyRecord, err := hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord, "Alert history record should exist")
assert.Equal(t, alert.Id, historyRecord.GetString("alert_id"), "Alert history should reference correct alert")
assert.Equal(t, system.Id, historyRecord.GetString("system"), "Alert history should reference correct system")
assert.Equal(t, "Status", historyRecord.GetString("name"), "Alert history should have correct name")
// The alert history might be resolved immediately in some cases, so let's check the alert's triggered status
alertRecord, err := hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alert.Id})
assert.NoError(t, err)
assert.True(t, alertRecord.GetBool("triggered"), "Alert should still be triggered when checking history")
// Now resolve the alert by setting system back to up
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(200 * time.Millisecond)
// Check that alert is no longer triggered
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert.Id})
assert.NoError(t, err)
assert.Zero(t, triggeredCount, "Alert should not be triggered after system is back up")
// Check that alert history record is now resolved
historyRecord, err = hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord, "Alert history record should still exist")
assert.NotNil(t, historyRecord.Get("resolved"), "Alert history should be resolved")
// Test deleting a triggered alert resolves its history
// Create another system and alert
systems2, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system2 := systems2[0]
system2.Set("name", "test-system-2") // Rename for clarity
err = hub.SaveNoValidate(system2)
assert.NoError(t, err)
alert2, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system2.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Set system2 to down to trigger alert
system2.Set("status", "down")
err = hub.SaveNoValidate(system2)
assert.NoError(t, err)
// Wait for alert to trigger
time.Sleep(time.Second * 75)
// Verify alert is triggered and history record exists
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert2.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "Second alert should be triggered")
historyCount, err = hub.CountRecords("alerts_history", dbx.HashExp{"alert_id": alert2.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, historyCount, "Should have 1 alert history record for second alert")
// Delete the triggered alert
err = hub.Delete(alert2)
assert.NoError(t, err)
// Check that alert history record is resolved after deletion
historyRecord2, err := hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert2.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord2, "Alert history record should still exist after alert deletion")
assert.NotNil(t, historyRecord2.Get("resolved"), "Alert history should be resolved after alert deletion")
// Verify total history count is correct (2 records total)
totalHistoryCount, err := hub.CountRecords("alerts_history", nil)
assert.NoError(t, err)
assert.EqualValues(t, 2, totalHistoryCount, "Should have 2 total alert history records")
})
}

View File

@@ -1,55 +0,0 @@
package alerts
import (
"sync"
"time"
"github.com/pocketbase/pocketbase/core"
)
func (am *AlertManager) GetAlertManager() *AlertManager {
return am
}
func (am *AlertManager) GetPendingAlerts() *sync.Map {
return &am.pendingAlerts
}
func (am *AlertManager) GetPendingAlertsCount() int {
count := 0
am.pendingAlerts.Range(func(key, value any) bool {
count++
return true
})
return count
}
// ProcessPendingAlerts manually processes all expired alerts (for testing)
func (am *AlertManager) ProcessPendingAlerts() ([]*core.Record, error) {
now := time.Now()
var lastErr error
var processedAlerts []*core.Record
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
if now.After(info.expireTime) {
// Downtime delay has passed, process alert
if err := am.sendStatusAlert("down", info.systemName, info.alertRecord); err != nil {
lastErr = err
}
processedAlerts = append(processedAlerts, info.alertRecord)
am.pendingAlerts.Delete(key)
}
return true
})
return processedAlerts, lastErr
}
// ForceExpirePendingAlerts sets all pending alerts to expire immediately (for testing)
func (am *AlertManager) ForceExpirePendingAlerts() {
now := time.Now()
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
info.expireTime = now.Add(-time.Second) // Set to 1 second ago
return true
})
}

View File

@@ -39,7 +39,6 @@ type Stats struct {
// 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 {

View File

@@ -1,140 +0,0 @@
package ghupdate
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// extract extracts an archive file to the destination directory.
// Supports .zip and .tar.gz files based on the file extension.
func extract(srcPath, destDir string) error {
if strings.HasSuffix(srcPath, ".tar.gz") {
return extractTarGz(srcPath, destDir)
}
// Default to zip extraction
return extractZip(srcPath, destDir)
}
// extractTarGz extracts a tar.gz archive to the destination directory.
func extractTarGz(srcPath, destDir string) error {
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
gz, err := gzip.NewReader(src)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if header.Typeflag == tar.TypeDir {
if err := os.MkdirAll(filepath.Join(destDir, header.Name), 0755); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(filepath.Join(destDir, header.Name)), 0755); err != nil {
return err
}
outFile, err := os.Create(filepath.Join(destDir, header.Name))
if err != nil {
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return err
}
outFile.Close()
}
return nil
}
// extractZip extracts the zip archive at "src" to "dest".
//
// Note that only dirs and regular files will be extracted.
// Symbolic links, named pipes, sockets, or any other irregular files
// are skipped because they come with too many edge cases and ambiguities.
func extractZip(src, dest string) error {
zr, err := zip.OpenReader(src)
if err != nil {
return err
}
defer zr.Close()
// normalize dest path to check later for Zip Slip
dest = filepath.Clean(dest) + string(os.PathSeparator)
for _, f := range zr.File {
err := extractFile(f, dest)
if err != nil {
return err
}
}
return nil
}
// extractFile extracts the provided zipFile into "basePath/zipFileName" path,
// creating all the necessary path directories.
func extractFile(zipFile *zip.File, basePath string) error {
path := filepath.Join(basePath, zipFile.Name)
// check for Zip Slip
if !strings.HasPrefix(path, basePath) {
return fmt.Errorf("invalid file path: %s", path)
}
r, err := zipFile.Open()
if err != nil {
return err
}
defer r.Close()
// allow only dirs or regular files
if zipFile.FileInfo().IsDir() {
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return err
}
} else if zipFile.FileInfo().Mode().IsRegular() {
// ensure that the file path directories are created
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, r)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,348 +0,0 @@
// Package ghupdate implements a new command to self update the current
// executable with the latest GitHub release. This is based on PocketBase's
// ghupdate package with modifications.
package ghupdate
import (
"beszel"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/blang/semver"
)
// Minimal color functions using ANSI escape codes
const (
colorReset = "\033[0m"
ColorYellow = "\033[33m"
ColorGreen = "\033[32m"
colorCyan = "\033[36m"
colorGray = "\033[90m"
)
func ColorPrint(color, text string) {
fmt.Println(color + text + colorReset)
}
func ColorPrintf(color, format string, args ...interface{}) {
fmt.Printf(color+format+colorReset+"\n", args...)
}
// HttpClient is a base HTTP client interface (usually used for test purposes).
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// Config defines the config options of the ghupdate plugin.
//
// NB! This plugin is considered experimental and its config options may change in the future.
type Config struct {
// Owner specifies the account owner of the repository (default to "pocketbase").
Owner string
// Repo specifies the name of the repository (default to "pocketbase").
Repo string
// ArchiveExecutable specifies the name of the executable file in the release archive
// (default to "pocketbase"; an additional ".exe" check is also performed as a fallback).
ArchiveExecutable string
// Optional context to use when fetching and downloading the latest release.
Context context.Context
// The HTTP client to use when fetching and downloading the latest release.
// Defaults to `http.DefaultClient`.
HttpClient HttpClient
// The data directory to use when fetching and downloading the latest release.
DataDir string
// UseMirror specifies whether to use the beszel.dev mirror instead of GitHub API.
// When false (default), always uses api.github.com. When true, uses gh.beszel.dev.
UseMirror bool
}
type updater struct {
config Config
currentVersion string
}
func Update(config Config) (updated bool, err error) {
p := &updater{
currentVersion: beszel.Version,
config: config,
}
return p.update()
}
func (p *updater) update() (updated bool, err error) {
ColorPrint(ColorYellow, "Fetching release information...")
if p.config.DataDir == "" {
p.config.DataDir = os.TempDir()
}
if p.config.Owner == "" {
p.config.Owner = "henrygd"
}
if p.config.Repo == "" {
p.config.Repo = "beszel"
}
if p.config.Context == nil {
p.config.Context = context.Background()
}
if p.config.HttpClient == nil {
p.config.HttpClient = http.DefaultClient
}
var latest *release
var useMirror bool
// Determine the API endpoint based on UseMirror flag
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", p.config.Owner, p.config.Repo)
if p.config.UseMirror {
useMirror = true
apiURL = fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", p.config.Owner, p.config.Repo)
ColorPrint(ColorYellow, "Using mirror for update.")
}
latest, err = fetchLatestRelease(
p.config.Context,
p.config.HttpClient,
apiURL,
)
if err != nil {
return false, err
}
currentVersion := semver.MustParse(strings.TrimPrefix(p.currentVersion, "v"))
newVersion := semver.MustParse(strings.TrimPrefix(latest.Tag, "v"))
if newVersion.LTE(currentVersion) {
ColorPrintf(ColorGreen, "You already have the latest version %s.", p.currentVersion)
return false, nil
}
suffix := archiveSuffix(p.config.ArchiveExecutable, runtime.GOOS, runtime.GOARCH)
asset, err := latest.findAssetBySuffix(suffix)
if err != nil {
return false, err
}
releaseDir := filepath.Join(p.config.DataDir, ".beszel_update")
defer os.RemoveAll(releaseDir)
ColorPrintf(ColorYellow, "Downloading %s...", asset.Name)
// download the release asset
assetPath := filepath.Join(releaseDir, asset.Name)
if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath, useMirror); err != nil {
return false, err
}
ColorPrintf(ColorYellow, "Extracting %s...", asset.Name)
extractDir := filepath.Join(releaseDir, "extracted_"+asset.Name)
defer os.RemoveAll(extractDir)
// Extract the archive (automatically detects format)
if err := extract(assetPath, extractDir); err != nil {
return false, err
}
ColorPrint(ColorYellow, "Replacing the executable...")
oldExec, err := os.Executable()
if err != nil {
return false, err
}
renamedOldExec := oldExec + ".old"
defer os.Remove(renamedOldExec)
newExec := filepath.Join(extractDir, p.config.ArchiveExecutable)
if _, err := os.Stat(newExec); err != nil {
// try again with an .exe extension
newExec = newExec + ".exe"
if _, fallbackErr := os.Stat(newExec); fallbackErr != nil {
return false, fmt.Errorf("the executable in the extracted path is missing or it is inaccessible: %v, %v", err, fallbackErr)
}
}
// rename the current executable
if err := os.Rename(oldExec, renamedOldExec); err != nil {
return false, fmt.Errorf("failed to rename the current executable: %w", err)
}
tryToRevertExecChanges := func() {
if revertErr := os.Rename(renamedOldExec, oldExec); revertErr != nil {
slog.Debug(
"Failed to revert executable",
slog.String("old", renamedOldExec),
slog.String("new", oldExec),
slog.String("error", revertErr.Error()),
)
}
}
// replace with the extracted binary
if err := os.Rename(newExec, oldExec); err != nil {
// If rename fails due to cross-device link, try copying instead
if isCrossDeviceError(err) {
if err := copyFile(newExec, oldExec); err != nil {
tryToRevertExecChanges()
return false, fmt.Errorf("failed replacing the executable: %w", err)
}
} else {
tryToRevertExecChanges()
return false, fmt.Errorf("failed replacing the executable: %w", err)
}
}
ColorPrint(colorGray, "---")
ColorPrint(ColorGreen, "Update completed successfully!")
// print the release notes
if latest.Body != "" {
fmt.Print("\n")
releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1))
ColorPrint(colorCyan, releaseNotes)
fmt.Print("\n")
}
return true, nil
}
func fetchLatestRelease(
ctx context.Context,
client HttpClient,
url string,
) (*release, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
rawBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
// http.Client doesn't treat non 2xx responses as error
if res.StatusCode >= 400 {
return nil, fmt.Errorf(
"(%d) failed to fetch latest releases:\n%s",
res.StatusCode,
string(rawBody),
)
}
result := &release{}
if err := json.Unmarshal(rawBody, result); err != nil {
return nil, err
}
return result, nil
}
func downloadFile(
ctx context.Context,
client HttpClient,
url string,
destPath string,
useMirror bool,
) error {
if useMirror {
url = strings.Replace(url, "github.com", "gh.beszel.dev", 1)
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// http.Client doesn't treat non 2xx responses as error
if res.StatusCode >= 400 {
return fmt.Errorf("(%d) failed to send download file request", res.StatusCode)
}
// ensure that the dest parent dir(s) exist
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
dest, err := os.Create(destPath)
if err != nil {
return err
}
defer dest.Close()
if _, err := io.Copy(dest, res.Body); err != nil {
return err
}
return nil
}
// isCrossDeviceError checks if the error is due to a cross-device link
func isCrossDeviceError(err error) bool {
return err != nil && (strings.Contains(err.Error(), "cross-device") ||
strings.Contains(err.Error(), "EXDEV"))
}
// copyFile copies a file from src to dst, preserving permissions
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
// Copy the file contents
if _, err := io.Copy(destFile, sourceFile); err != nil {
return err
}
// Preserve the original file permissions
sourceInfo, err := sourceFile.Stat()
if err != nil {
return err
}
return destFile.Chmod(sourceInfo.Mode())
}
func archiveSuffix(binaryName, goos, goarch string) string {
if goos == "windows" {
return fmt.Sprintf("%s_%s_%s.zip", binaryName, goos, goarch)
}
return fmt.Sprintf("%s_%s_%s.tar.gz", binaryName, goos, goarch)
}

View File

@@ -1,45 +0,0 @@
package ghupdate
import (
"path/filepath"
"testing"
)
func TestReleaseFindAssetBySuffix(t *testing.T) {
r := release{
Assets: []*releaseAsset{
{Name: "test1.zip", Id: 1},
{Name: "test2.zip", Id: 2},
{Name: "test22.zip", Id: 22},
{Name: "test3.zip", Id: 3},
},
}
asset, err := r.findAssetBySuffix("2.zip")
if err != nil {
t.Fatalf("Expected nil, got err: %v", err)
}
if asset.Id != 2 {
t.Fatalf("Expected asset with id %d, got %v", 2, asset)
}
}
func TestExtractFailure(t *testing.T) {
testDir := t.TempDir()
// Test with missing zip file
missingZipPath := filepath.Join(testDir, "missing_test.zip")
extractedPath := filepath.Join(testDir, "zip_extract")
if err := extract(missingZipPath, extractedPath); err == nil {
t.Fatal("Expected Extract to fail due to missing zip file")
}
// Test with missing tar.gz file
missingTarPath := filepath.Join(testDir, "missing_test.tar.gz")
if err := extract(missingTarPath, extractedPath); err == nil {
t.Fatal("Expected Extract to fail due to missing tar.gz file")
}
}

View File

@@ -1,36 +0,0 @@
package ghupdate
import (
"errors"
"strings"
)
type releaseAsset struct {
Name string `json:"name"`
DownloadUrl string `json:"browser_download_url"`
Id int `json:"id"`
Size int `json:"size"`
}
type release struct {
Name string `json:"name"`
Tag string `json:"tag_name"`
Published string `json:"published_at"`
Url string `json:"html_url"`
Body string `json:"body"`
Assets []*releaseAsset `json:"assets"`
Id int `json:"id"`
}
// findAssetBySuffix returns the first available asset containing the specified suffix.
func (r *release) findAssetBySuffix(suffix string) (*releaseAsset, error) {
if suffix != "" {
for _, asset := range r.Assets {
if strings.HasSuffix(asset.Name, suffix) {
return asset, nil
}
}
}
return nil, errors.New("missing asset containing " + suffix)
}

View File

@@ -8,10 +8,13 @@ import (
"beszel/internal/hub/systems"
"beszel/internal/records"
"beszel/internal/users"
"beszel/site"
"crypto/ed25519"
"encoding/pem"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
@@ -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
@@ -163,6 +164,55 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
return nil
}
// startServer sets up the server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
// TODO: exclude dev server from production binary
switch h.IsDev() {
case true:
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
default:
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
indexContent = strings.Replace(indexContent, "{{V}}", beszel.Version, 1)
indexContent = strings.Replace(indexContent, "{{HUB_URL}}", h.appURL, 1)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP")
// add route
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, indexContent)
})
}
return nil
}
// registerCronJobs sets up scheduled tasks
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
// delete old system_stats and alerts_history records once every hour

View File

@@ -5,7 +5,6 @@ package hub_test
import (
beszelTests "beszel/internal/tests"
"beszel/migrations"
"testing"
"bytes"
@@ -535,115 +534,6 @@ func TestApiRoutesAuthentication(t *testing.T) {
}
}
func TestFirstUserCreation(t *testing.T) {
t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
testAppFactoryExisting := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenarios := []beszelTests.ApiScenario{
{
Name: "POST /create-user - should be available when no users exist",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
Body: jsonReader(map[string]any{
"email": "firstuser@example.com",
"password": "password123",
}),
ExpectedStatus: 200,
ExpectedContent: []string{"User created"},
TestAppFactory: testAppFactoryExisting,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
userCount, err := hub.CountRecords("users")
require.NoError(t, err)
require.Zero(t, userCount, "Should start with no users")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should start with one temporary superuser")
require.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString("email"), "Should have created one temporary superuser")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
userCount, err := hub.CountRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, userCount, "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should have created one superuser")
require.EqualValues(t, "firstuser@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
},
{
Name: "POST /create-user - should not be available when users exist",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
Body: jsonReader(map[string]any{
"email": "firstuser@example.com",
"password": "password123",
}),
ExpectedStatus: 404,
ExpectedContent: []string{"wasn't found"},
TestAppFactory: testAppFactoryExisting,
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
})
t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) {
os.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com")
os.Setenv("BESZEL_HUB_USER_PASSWORD", "password123")
defer os.Unsetenv("BESZEL_HUB_USER_EMAIL")
defer os.Unsetenv("BESZEL_HUB_USER_PASSWORD")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
testAppFactory := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenario := beszelTests.ApiScenario{
Name: "POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
ExpectedStatus: 404,
ExpectedContent: []string{"wasn't found"},
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
users, err := hub.FindAllRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, len(users), "Should start with one user")
require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should start with one superuser")
require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
users, err := hub.FindAllRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, len(users), "Should still have one user")
require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should still have one superuser")
require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
}
scenario.Test(t)
})
}
func TestCreateUserEndpointAvailability(t *testing.T) {
t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())

View File

@@ -1,79 +0,0 @@
//go:build development
package hub
import (
"beszel"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/pocketbase/pocketbase/core"
)
// Wraps http.RoundTripper to modify dev proxy HTML responses
type responseModifier struct {
transport http.RoundTripper
hub *Hub
}
func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := rm.transport.RoundTrip(req)
if err != nil {
return resp, err
}
// Only modify HTML responses
contentType := resp.Header.Get("Content-Type")
if !strings.Contains(contentType, "text/html") {
return resp, nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp, err
}
resp.Body.Close()
// Create a new response with the modified body
modifiedBody := rm.modifyHTML(string(body))
resp.Body = io.NopCloser(strings.NewReader(modifiedBody))
resp.ContentLength = int64(len(modifiedBody))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody)))
return resp, nil
}
func (rm *responseModifier) modifyHTML(html string) string {
parsedURL, err := url.Parse(rm.hub.appURL)
if err != nil {
return html
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
html = strings.ReplaceAll(html, "./", basePath)
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
html = strings.Replace(html, "{{HUB_URL}}", rm.hub.appURL, 1)
return html
}
// startServer sets up the development server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
slog.Info("starting server", "appURL", h.appURL)
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
proxy.Transport = &responseModifier{
transport: http.DefaultTransport,
hub: h,
}
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
return nil
}

View File

@@ -1,51 +0,0 @@
//go:build !development
package hub
import (
"beszel"
"beszel/site"
"io/fs"
"net/http"
"net/url"
"strings"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)
// startServer sets up the production server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
html := strings.ReplaceAll(string(indexFile), "./", basePath)
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
html = strings.Replace(html, "{{HUB_URL}}", h.appURL, 1)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP")
// add route
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, html)
})
return nil
}

View File

@@ -29,7 +29,7 @@ func TestSystemManagerNew(t *testing.T) {
user, err := tests.CreateUser(hub, "test@test.com", "testtesttest")
require.NoError(t, err)
synctest.Test(t, func(t *testing.T) {
synctest.Run(func() {
sm.Initialize()
record, err := tests.CreateRecord(hub, "systems", map[string]any{
@@ -110,11 +110,9 @@ func TestSystemManagerNew(t *testing.T) {
err = hub.Delete(record)
require.NoError(t, err)
assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after deletion")
})
testOld(t, hub)
testOld(t, hub)
synctest.Test(t, func(t *testing.T) {
time.Sleep(time.Second)
synctest.Wait()

View File

@@ -100,10 +100,3 @@ func (sm *SystemManager) SetSystemStatusInDB(systemID string, status string) boo
return true
}
// TESTING ONLY: RemoveAllSystems removes all systems from the store
func (sm *SystemManager) RemoveAllSystems() {
for _, system := range sm.systems.GetAll() {
sm.RemoveSystem(system.Id)
}
}

View File

@@ -1,85 +1,57 @@
package hub
import (
"beszel/internal/ghupdate"
"beszel"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/spf13/cobra"
)
// Update updates beszel to the latest version
func Update(cmd *cobra.Command, _ []string) {
dataDir := os.TempDir()
// set dataDir to ./beszel_data if it exists
if _, err := os.Stat("./beszel_data"); err == nil {
dataDir = "./beszel_data"
}
// Check if china-mirrors flag is set
useMirror, _ := cmd.Flags().GetBool("china-mirrors")
updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel",
DataDir: dataDir,
UseMirror: useMirror,
func Update(_ *cobra.Command, _ []string) {
var latest *selfupdate.Release
var found bool
var err error
currentVersion := semver.MustParse(beszel.Version)
fmt.Println("beszel", currentVersion)
fmt.Println("Checking for updates...")
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
Filters: []string{"beszel_"},
})
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil {
log.Fatal(err)
fmt.Println("Error checking for updates:", err)
os.Exit(1)
}
if !updated {
if !found {
fmt.Println("No updates found")
os.Exit(0)
}
fmt.Println("Latest version:", latest.Version)
if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return
}
// make sure the file is executable
exePath, err := os.Executable()
if err == nil {
if err := os.Chmod(exePath, 0755); err != nil {
fmt.Printf("Warning: failed to set executable permissions: %v\n", err)
}
var binaryPath string
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
binaryPath, err = os.Executable()
if err != nil {
fmt.Println("Error getting binary path:", err)
os.Exit(1)
}
// Try to restart the service if it's running
restartService()
}
// restartService attempts to restart the beszel service
func restartService() {
// Check if we're running as a service by looking for systemd
if _, err := exec.LookPath("systemctl"); err == nil {
// Check if beszel service exists and is active
cmd := exec.Command("systemctl", "is-active", "beszel.service")
if err := cmd.Run(); err == nil {
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("systemctl", "restart", "beszel.service")
if err := restartCmd.Run(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: Failed to restart service: %v\n", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually: sudo systemctl restart beszel")
} else {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
return
}
}
// Check for OpenRC (Alpine Linux)
if _, err := exec.LookPath("rc-service"); err == nil {
cmd := exec.Command("rc-service", "beszel", "status")
if err := cmd.Run(); err == nil {
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("rc-service", "beszel", "restart")
if err := restartCmd.Run(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: Failed to restart service: %v\n", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually: sudo rc-service beszel restart")
} else {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
return
}
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Service restart not attempted. If running as a service, restart manually.")
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
if err != nil {
fmt.Println("Please try rerunning with sudo. Error:", err)
os.Exit(1)
}
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
}

View File

@@ -214,7 +214,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Battery[1] = stats.Battery[1]
// Set peak values
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)

View File

@@ -96,31 +96,3 @@ func ClearCollection(t testing.TB, app core.App, collectionName string) error {
assert.EqualValues(t, recordCount, 0, "should have 0 records after clearing")
return err
}
func (h *TestHub) Cleanup() {
h.GetAlertManager().StopWorker()
h.GetSystemManager().RemoveAllSystems()
h.TestApp.Cleanup()
}
func CreateSystems(app core.App, count int, userId string, status string) ([]*core.Record, error) {
systems := make([]*core.Record, 0, count)
for i := range count {
system, err := CreateRecord(app, "systems", map[string]any{
"name": fmt.Sprintf("test-system-%d", i),
"host": fmt.Sprintf("127.0.0.%d", i),
"port": "33914",
"users": []string{userId},
})
if err != nil {
return nil, err
}
system.Set("status", status)
err = app.SaveNoValidate(system)
if err != nil {
return nil, err
}
systems = append(systems, system)
}
return systems, nil
}

View File

@@ -1,8 +1,6 @@
package migrations
import (
"os"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
@@ -21,51 +19,11 @@ func init() {
if err := app.Save(settings); err != nil {
return err
}
// create superuser
superuserCollection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
superUser := core.NewRecord(superuserCollection)
// set email
email, _ := GetEnv("USER_EMAIL")
password, _ := GetEnv("USER_PASSWORD")
didProvideUserDetails := email != "" && password != ""
// set superuser email
if email == "" {
email = TempAdminEmail
}
superUser.SetEmail(email)
// set superuser password
if password != "" {
superUser.SetPassword(password)
} else {
superUser.SetRandomPassword()
}
// if user details are provided, we create a regular user as well
if didProvideUserDetails {
usersCollection, _ := app.FindCollectionByNameOrId("users")
user := core.NewRecord(usersCollection)
user.SetEmail(email)
user.SetPassword(password)
user.SetVerified(true)
err := app.Save(user)
if err != nil {
return err
}
}
return app.Save(superUser)
collection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
user := core.NewRecord(collection)
user.SetEmail(TempAdminEmail)
user.SetRandomPassword()
return app.Save(user)
}, nil)
}
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
func GetEnv(key string) (value string, exists bool) {
if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists {
return value, exists
}
// Fallback to the old unprefixed key
return os.LookupEnv(key)
}

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.12.7",
"version": "0.12.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.12.7",
"version": "0.12.3",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -30,7 +30,6 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -3131,23 +3130,6 @@
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
@@ -3161,16 +3143,6 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/bun": {
"version": "1.2.20",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.20.tgz",

View File

@@ -1,10 +1,10 @@
{
"name": "beszel",
"private": true,
"version": "0.12.7",
"version": "0.12.3",
"type": "module",
"scripts": {
"dev": "vite --host",
"dev": "vite",
"build": "lingui extract --overwrite && lingui compile && vite build",
"preview": "vite preview",
"sync": "lingui extract --overwrite && lingui compile",
@@ -33,7 +33,6 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",

View File

@@ -13,9 +13,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { $publicKey } from "@/lib/stores"
import { cn, generateToken, tokenMap, useBrowserStorage } from "@/lib/utils"
import { pb, isReadOnlyUser } from "@/lib/api"
import { $publicKey, pb } from "@/lib/stores"
import { cn, generateToken, isReadOnlyUser, tokenMap, useLocalStorage } from "@/lib/utils"
import { useStore } from "@nanostores/react"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react"
@@ -77,7 +76,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
const port = useRef<HTMLInputElement>(null)
const [hostValue, setHostValue] = useState(system?.host ?? "")
const isUnixSocket = hostValue.startsWith("/")
const [tab, setTab] = useBrowserStorage("as-tab", "docker")
const [tab, setTab] = useLocalStorage("as-tab", "docker")
const [token, setToken] = useState(system?.token ?? "")
useEffect(() => {
@@ -97,7 +96,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
tokenMap.set(system.id, token)
setToken(token)
})()
}, [system?.id, nextSystemToken])
}, [system?.id])
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
@@ -133,7 +132,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
>
<Tabs defaultValue={tab} onValueChange={setTab}>
<DialogHeader>
<DialogTitle className="mb-2 max-w-100 truncate pr-8">
<DialogTitle className="mb-2">
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
</DialogTitle>
<TabsList className="grid w-full grid-cols-2">

View File

@@ -16,9 +16,7 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
<Trans>System</Trans>
</Button>
),
cell: ({ row }) => (
<div className="ps-2 max-w-60 truncate">{row.original.expand?.system?.name || row.original.system}</div>
),
cell: ({ row }) => <span className="ps-2">{row.original.expand?.system?.name || row.original.system}</span>,
filterFn: (row, _, filterValue) => {
const display = row.original.expand?.system?.name || row.original.system || ""
return display.toLowerCase().includes(filterValue.toLowerCase())

View File

@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans, Plural } from "@lingui/react/macro"
import { $alerts, $systems } from "@/lib/stores"
import { $alerts, $systems, pb } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { Switch } from "@/components/ui/switch"
@@ -15,7 +15,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { ServerIcon, GlobeIcon } from "lucide-react"
import { $router, Link } from "@/components/router"
import { DialogHeader } from "@/components/ui/dialog"
import { pb } from "@/lib/api"
const Slider = lazy(() => import("@/components/ui/slider"))
@@ -96,7 +95,7 @@ export const AlertDialogContent = memo(function AlertDialogContent({ system }: {
<TabsList className="mb-1 -mt-0.5">
<TabsTrigger value="system">
<ServerIcon className="me-2 h-3.5 w-3.5" />
<span className="truncate max-w-60">{system.name}</span>
{system.name}
</TabsTrigger>
<TabsTrigger value="global">
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />

View File

@@ -1,14 +1,13 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, formatShortDate, chartMargin } from "@/lib/utils"
import { useYAxisWidth } from "./hooks"
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
import { ChartData, SystemStatsRecord } from "@/types"
import { useMemo } from "react"
export type DataPoint = {
label: string
dataKey: (data: SystemStatsRecord) => number | undefined
color: number | string
color: string
opacity: number
}

View File

@@ -1,26 +1,23 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { memo, useMemo } from "react"
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
import { useYAxisWidth, cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
// import Spinner from '../spinner'
import { useStore } from "@nanostores/react"
import { $containerFilter, $userSettings } from "@/lib/stores"
import { ChartData } from "@/types"
import { Separator } from "../ui/separator"
import { ChartType, Unit } from "@/lib/enums"
import { useYAxisWidth } from "./hooks"
export default memo(function ContainerChart({
dataKey,
chartData,
chartType,
chartConfig,
unit = "%",
}: {
dataKey: string
chartData: ChartData
chartType: ChartType
chartConfig: ChartConfig
unit?: string
}) {
const filter = useStore($containerFilter)
@@ -31,6 +28,40 @@ export default memo(function ContainerChart({
const isNetChart = chartType === ChartType.Network
const chartConfig = useMemo(() => {
const config = {} as Record<string, { label: string; color: string }>
const totalUsage = new Map<string, number>()
// calculate total usage of each container
for (const stats of containerData) {
for (const key in stats) {
if (!key || key === "created") continue
const currentTotal = totalUsage.get(key) ?? 0
const increment = isNetChart
? (stats[key]?.nr ?? 0) + (stats[key]?.ns ?? 0)
: // @ts-ignore
stats[key]?.[dataKey] ?? 0
totalUsage.set(key, currentTotal + increment)
}
}
// Sort keys and generate colors based on usage
const sortedEntries = Array.from(totalUsage.entries()).sort(([, a], [, b]) => b - a)
const length = sortedEntries.length
sortedEntries.forEach(([key], i) => {
const hue = ((i * 360) / length) % 360
config[key] = {
label: key,
color: `hsl(${hue}, 60%, 55%)`,
}
})
return config satisfies ChartConfig
}, [chartData])
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
const obj = {} as {
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
@@ -88,14 +119,7 @@ export default memo(function ContainerChart({
return obj
}, [])
// Filter with set lookup
const filteredKeys = useMemo(() => {
if (!filter) {
return new Set<string>()
}
const filterLower = filter.toLowerCase()
return new Set(Object.keys(chartConfig).filter((key) => !key.toLowerCase().includes(filterLower)))
}, [chartConfig, filter])
const filterLower = filter?.toLowerCase()
// console.log('rendered at', new Date())
@@ -138,9 +162,9 @@ export default memo(function ContainerChart({
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
/>
{Object.keys(chartConfig).map((key) => {
const filtered = filteredKeys.has(key)
const fillOpacity = filtered ? 0.05 : 0.4
const strokeOpacity = filtered ? 0.1 : 1
const filtered = filterLower && !key.toLowerCase().includes(filterLower)
let fillOpacity = filtered ? 0.05 : 0.4
let strokeOpacity = filtered ? 0.1 : 1
return (
<Area
key={key}

View File

@@ -1,11 +1,10 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo } from "react"
import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
import { useYAxisWidth } from "./hooks"
export default memo(function DiskChart({
dataKey,

View File

@@ -8,10 +8,9 @@ import {
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo, useMemo } from "react"
import { useYAxisWidth } from "./hooks"
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -1,107 +0,0 @@
import { useMemo, useState } from "react"
import { ChartConfig } from "@/components/ui/chart"
import { ChartData } from "@/types"
/** Chart configurations for CPU, memory, and network usage charts */
export interface ContainerChartConfigs {
cpu: ChartConfig
memory: ChartConfig
network: ChartConfig
}
/**
* Generates chart configurations for container metrics visualization
* @param containerData - Array of container statistics data points
* @returns Chart configurations for CPU, memory, and network metrics
*/
export function useContainerChartConfigs(containerData: ChartData["containerData"]): ContainerChartConfigs {
return useMemo(() => {
const configs = {
cpu: {} as ChartConfig,
memory: {} as ChartConfig,
network: {} as ChartConfig,
}
// Aggregate usage metrics for each container
const totalUsage = {
cpu: new Map<string, number>(),
memory: new Map<string, number>(),
network: new Map<string, number>(),
}
// Process each data point to calculate totals
for (let i = 0; i < containerData.length; i++) {
const stats = containerData[i]
const containerNames = Object.keys(stats)
for (let j = 0; j < containerNames.length; j++) {
const containerName = containerNames[j]
// Skip metadata field
if (containerName === "created") {
continue
}
const containerStats = stats[containerName]
if (!containerStats) {
continue
}
// Accumulate metrics for CPU, memory, and network
const currentCpu = totalUsage.cpu.get(containerName) ?? 0
const currentMemory = totalUsage.memory.get(containerName) ?? 0
const currentNetwork = totalUsage.network.get(containerName) ?? 0
totalUsage.cpu.set(containerName, currentCpu + (containerStats.c ?? 0))
totalUsage.memory.set(containerName, currentMemory + (containerStats.m ?? 0))
totalUsage.network.set(containerName, currentNetwork + (containerStats.nr ?? 0) + (containerStats.ns ?? 0))
}
}
// Generate chart configurations for each metric type
Object.entries(totalUsage).forEach(([chartType, usageMap]) => {
const sortedContainers = Array.from(usageMap.entries()).sort(([, a], [, b]) => b - a)
const chartConfig = {} as Record<string, { label: string; color: string }>
const count = sortedContainers.length
// Generate colors for each container
for (let i = 0; i < count; i++) {
const [containerName] = sortedContainers[i]
const hue = ((i * 360) / count) % 360
chartConfig[containerName] = {
label: containerName,
color: `hsl(${hue}, 60%, 55%)`,
}
}
configs[chartType as keyof typeof configs] = chartConfig
})
return configs
}, [containerData])
}
/** Sets the correct width of the y axis in recharts based on the longest label */
export function useYAxisWidth() {
const [yAxisWidth, setYAxisWidth] = useState(0)
let maxChars = 0
let timeout: ReturnType<typeof setTimeout>
function updateYAxisWidth(str: string) {
if (str.length > maxChars) {
maxChars = str.length
const div = document.createElement("div")
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
div.innerHTML = str
clearTimeout(timeout)
timeout = setTimeout(() => {
document.body.appendChild(div)
const width = div.offsetWidth + 24
if (width > yAxisWidth) {
setYAxisWidth(div.offsetWidth + 24)
}
document.body.removeChild(div)
})
}
return str
}
return { yAxisWidth, updateYAxisWidth }
}

View File

@@ -8,11 +8,10 @@ import {
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData, SystemStats } from "@/types"
import { memo } from "react"
import { t } from "@lingui/core/macro"
import { useYAxisWidth } from "./hooks"
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -1,13 +1,12 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { useYAxisWidth, cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { memo } from "react"
import { ChartData } from "@/types"
import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
import { useYAxisWidth } from "./hooks"
export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { t } = useLingui()
@@ -67,7 +66,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
<Area
name={t`Used`}
order={3}
dataKey={({ stats }) => (showMax ? stats?.mm : stats?.mu)}
dataKey="stats.mu"
type="monotoneX"
fill="var(--chart-2)"
fillOpacity={0.4}
@@ -75,31 +74,31 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
stackId="1"
isAnimationActive={false}
/>
{/* {chartData.systemStats.at(-1)?.stats.mz && ( */}
<Area
name="ZFS ARC"
order={2}
dataKey={({ stats }) => (showMax ? null : stats?.mz)}
type="monotoneX"
fill="hsla(175 60% 45% / 0.8)"
fillOpacity={0.5}
stroke="hsla(175 60% 45% / 0.8)"
stackId="1"
isAnimationActive={false}
/>
{/* )} */}
{chartData.systemStats.at(-1)?.stats.mz && (
<Area
name="ZFS ARC"
order={2}
dataKey="stats.mz"
type="monotoneX"
fill="hsla(175 60% 45% / 0.8)"
fillOpacity={0.5}
stroke="hsla(175 60% 45% / 0.8)"
stackId="1"
isAnimationActive={false}
/>
)}
<Area
name={t`Cache / Buffers`}
order={1}
dataKey={({ stats }) => (showMax ? null : stats?.mb)}
dataKey="stats.mb"
type="monotoneX"
fill="hsla(160 60% 45% / 0.5)"
fillOpacity={0.4}
// strokeOpacity={1}
stroke="hsla(160 60% 45% / 0.5)"
stackId="1"
isAnimationActive={false}
/>
{/* <ChartLegend content={<ChartLegendContent />} /> */}
</AreaChart>
</ChartContainer>
</div>

View File

@@ -2,12 +2,11 @@ import { t } from "@lingui/core/macro"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartData } from "@/types"
import { memo } from "react"
import { $userSettings } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { useYAxisWidth } from "./hooks"
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -8,12 +8,19 @@ import {
ChartTooltipContent,
xAxis,
} from "@/components/ui/chart"
import { cn, formatShortDate, toFixedFloat, chartMargin, formatTemperature, decimalString } from "@/lib/utils"
import {
useYAxisWidth,
cn,
formatShortDate,
toFixedFloat,
chartMargin,
formatTemperature,
decimalString,
} from "@/lib/utils"
import { ChartData } from "@/types"
import { memo, useMemo } from "react"
import { $temperatureFilter, $userSettings } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { useYAxisWidth } from "./hooks"
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
const filter = useStore($temperatureFilter)

View File

@@ -23,13 +23,11 @@ import {
} from "@/components/ui/command"
import { memo, useEffect, useMemo } from "react"
import { $systems } from "@/lib/stores"
import { getHostDisplayValue, listen } from "@/lib/utils"
import { getHostDisplayValue, isAdmin, listen } from "@/lib/utils"
import { $router, basePath, navigate, prependBasePath } from "./router"
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { getPagePath } from "@nanostores/router"
import { DialogDescription } from "@radix-ui/react-dialog"
import { isAdmin } from "@/lib/api"
export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
useEffect(() => {
@@ -56,9 +54,11 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
)
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<DialogDescription className="sr-only">Command palette</DialogDescription>
<CommandInput placeholder={t`Search for systems or settings...`} />
<CommandList>
<CommandEmpty>
<Trans>No results found.</Trans>
</CommandEmpty>
{systems.length > 0 && (
<>
<CommandGroup>
@@ -71,7 +71,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
}}
>
<Server className="me-2 size-4" />
<span className="max-w-60 truncate">{system.name}</span>
<span>{system.name}</span>
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
</CommandItem>
))}
@@ -121,7 +121,6 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
{SettingsShortcut}
</CommandItem>
<CommandItem
keywords={[t`Universal token`]}
onSelect={() => {
navigate(getPagePath($router, "settings", { name: "tokens" }))
setOpen(false)
@@ -215,9 +214,6 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
</CommandGroup>
</>
)}
<CommandEmpty>
<Trans>No results found.</Trans>
</CommandEmpty>
</CommandList>
</CommandDialog>
)

View File

@@ -22,7 +22,7 @@ export function LangToggle() {
{languages.map(({ lang, label, e }) => (
<DropdownMenuItem
key={lang}
className={cn("px-2.5 flex gap-2.5 cursor-pointer", lang === i18n.locale && "bg-accent/70 font-medium")}
className={cn("px-2.5 flex gap-2.5", lang === i18n.locale && "font-semibold")}
onClick={() => dynamicActivate(lang)}
>
<span>{e}</span> {label}

View File

@@ -5,7 +5,7 @@ import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
import { $authenticated } from "@/lib/stores"
import { $authenticated, pb } from "@/lib/stores"
import * as v from "valibot"
import { toast } from "../ui/use-toast"
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
@@ -13,7 +13,6 @@ import { useCallback, useEffect, useState } from "react"
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
import { $router, Link, prependBasePath } from "../router"
import { getPagePath } from "@nanostores/router"
import { pb } from "@/lib/api"
const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))

View File

@@ -1,5 +1,5 @@
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro";
import { t } from "@lingui/core/macro";
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
import { Input } from "../ui/input"
import { Label } from "../ui/label"
@@ -7,9 +7,9 @@ import { useCallback, useState } from "react"
import { toast } from "../ui/use-toast"
import { buttonVariants } from "../ui/button"
import { cn } from "@/lib/utils"
import { pb } from "@/lib/stores"
import { Dialog, DialogHeader } from "../ui/dialog"
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
import { pb } from "@/lib/api"
const showLoginFaliedToast = () => {
toast({

View File

@@ -1,14 +1,13 @@
import { t } from "@lingui/core/macro"
import { t } from "@lingui/core/macro";
import { UserAuthForm } from "@/components/login/auth-form"
import { Logo } from "../logo"
import { useEffect, useMemo, useState } from "react"
import { pb } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import ForgotPassword from "./forgot-pass-form"
import { $router } from "../router"
import { AuthMethodsList } from "pocketbase"
import { useTheme } from "../theme-provider"
import { pb } from "@/lib/api"
import { ModeToggle } from "../mode-toggle"
export default function () {
const page = useStore($router)
@@ -51,11 +50,8 @@ export default function () {
<div
className="grid gap-5 w-full px-4 mx-auto"
// @ts-ignore
style={{ maxWidth: "22em", "--border": theme == "light" ? "hsl(30, 8%, 70%)" : "hsl(220, 3%, 25%)" }}
style={{ maxWidth: "22em", "--border": theme == "light" ? "30 8% 80%" : "220 3% 20%" }}
>
<div className="absolute top-3 right-3">
<ModeToggle />
</div>
<div className="text-center">
<h1 className="mb-3">
<Logo className="h-7 fill-foreground mx-auto" />

View File

@@ -15,8 +15,8 @@ import { $router, basePath, Link, prependBasePath } from "./router"
import { LangToggle } from "./lang-toggle"
import { ModeToggle } from "./mode-toggle"
import { Logo } from "./logo"
import { cn, runOnce } from "@/lib/utils"
import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api"
import { pb } from "@/lib/stores"
import { cn, isReadOnlyUser, isAdmin, logOut } from "@/lib/utils"
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -36,17 +36,12 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar() {
return (
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
<Link
href={basePath}
aria-label="Home"
className="p-2 ps-0 me-3"
onMouseEnter={runOnce(() => import("@/components/routes/home"))}
>
<Link href={basePath} aria-label="Home" className="p-2 ps-0 me-3">
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
</Link>
<SearchButton />
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
<div className="flex items-center ms-auto">
<LangToggle />
<ModeToggle />
<Link

View File

@@ -1,16 +1,18 @@
import { Suspense, memo, useEffect, useMemo } from "react"
import { Suspense, lazy, memo, useEffect, useMemo } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $allSystemsById } from "@/lib/stores"
import { $alerts, $systems, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { GithubIcon } from "lucide-react"
import { Separator } from "../ui/separator"
import { AlertRecord } from "@/types"
import { getSystemNameFromId, updateRecordList, updateSystemList } from "@/lib/utils"
import { AlertRecord, SystemRecord } from "@/types"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { $router, Link } from "../router"
import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { getPagePath } from "@nanostores/router"
import { alertInfo } from "@/lib/alerts"
import SystemsTable from "@/components/systems-table/systems-table"
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default memo(function () {
const { t } = useLingui()
@@ -19,6 +21,19 @@ export default memo(function () {
document.title = t`Dashboard` + " / Beszel"
}, [t])
useEffect(() => {
// make sure we have the latest list of systems
updateSystemList()
// subscribe to real time updates for systems / alerts
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
updateRecordList(e, $systems)
})
return () => {
pb.collection("systems").unsubscribe("*")
}
}, [])
return useMemo(
() => (
<>
@@ -27,7 +42,7 @@ export default memo(function () {
<SystemsTable />
</Suspense>
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 text-xs opacity-80">
<a
href="https://github.com/henrygd/beszel"
target="_blank"
@@ -52,7 +67,6 @@ export default memo(function () {
const ActiveAlerts = () => {
const alerts = useStore($alerts)
const systems = useStore($allSystemsById)
const { activeAlerts, alertsKey } = useMemo(() => {
const activeAlerts: AlertRecord[] = []
@@ -96,7 +110,7 @@ const ActiveAlerts = () => {
>
<info.icon className="h-4 w-4" />
<AlertTitle>
{systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
{getSystemNameFromId(alert.system)} {info.name().toLowerCase().replace("cpu", "CPU")}
</AlertTitle>
<AlertDescription>
{alert.name === "Status" ? (
@@ -109,7 +123,7 @@ const ActiveAlerts = () => {
)}
</AlertDescription>
<Link
href={getPagePath($router, "system", { name: systems[alert.system]?.name })}
href={getPagePath($router, "system", { name: getSystemNameFromId(alert.system) })}
className="absolute inset-0 w-full h-full"
aria-label="View system"
></Link>

View File

@@ -1,4 +1,4 @@
import { pb } from "@/lib/api"
import { pb } from "@/lib/stores"
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { AlertsHistoryRecord } from "@/types"
@@ -273,13 +273,13 @@ export default function AlertsHistoryDataTable() {
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="border-border/50">
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead className="px-2" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</tr>
</TableRow>
))}
</TableHeader>
<TableBody>

View File

@@ -1,16 +1,17 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { isAdmin } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
import { Button } from "@/components/ui/button"
import { redirectPage } from "@nanostores/router"
import { $router } from "@/components/router"
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { pb } from "@/lib/stores"
import { useState } from "react"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
import clsx from "clsx"
import { isAdmin, pb } from "@/lib/api"
export default function ConfigYaml() {
const [configContent, setConfigContent] = useState<string>("")

View File

@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { lazy, useEffect } from "react"
import { useEffect } from "react"
import { Separator } from "../../ui/separator"
import { SidebarNav } from "./sidebar-nav.tsx"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
@@ -8,23 +8,15 @@ import { useStore } from "@nanostores/react"
import { $router } from "@/components/router.tsx"
import { getPagePath, redirectPage } from "@nanostores/router"
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
import { $userSettings } from "@/lib/stores.ts"
import { $userSettings, pb } from "@/lib/stores.ts"
import { toast } from "@/components/ui/use-toast.ts"
import { UserSettings } from "@/types"
import General from "./general.tsx"
import Notifications from "./notifications.tsx"
import ConfigYaml from "./config-yaml.tsx"
import { useLingui } from "@lingui/react/macro"
import { pb } from "@/lib/api"
const generalSettingsImport = () => import("./general.tsx")
const notificationsSettingsImport = () => import("./notifications.tsx")
const configYamlSettingsImport = () => import("./config-yaml.tsx")
const fingerprintsSettingsImport = () => import("./tokens-fingerprints.tsx")
const alertsHistoryDataTableSettingsImport = () => import("./alerts-history-data-table.tsx")
const GeneralSettings = lazy(generalSettingsImport)
const NotificationsSettings = lazy(notificationsSettingsImport)
const ConfigYamlSettings = lazy(configYamlSettingsImport)
const FingerprintsSettings = lazy(fingerprintsSettingsImport)
const AlertsHistoryDataTableSettings = lazy(alertsHistoryDataTableSettingsImport)
import Fingerprints from "./tokens-fingerprints.tsx"
import AlertsHistoryDataTable from "./alerts-history-data-table"
export async function saveSettings(newSettings: Partial<UserSettings>) {
try {
@@ -67,27 +59,23 @@ export default function SettingsLayout() {
title: t`Notifications`,
href: getPagePath($router, "settings", { name: "notifications" }),
icon: BellIcon,
preload: notificationsSettingsImport,
},
{
title: t`Tokens & Fingerprints`,
href: getPagePath($router, "settings", { name: "tokens" }),
icon: FingerprintIcon,
noReadOnly: true,
preload: fingerprintsSettingsImport,
},
{
title: t`Alert History`,
href: getPagePath($router, "settings", { name: "alert-history" }),
icon: AlertOctagonIcon,
preload: alertsHistoryDataTableSettingsImport,
},
{
title: t`YAML Config`,
href: getPagePath($router, "settings", { name: "config" }),
icon: FileSlidersIcon,
admin: true,
preload: configYamlSettingsImport,
},
]
@@ -102,7 +90,7 @@ export default function SettingsLayout() {
}, [])
return (
<Card className="pt-5 px-4 pb-8 min-h-96 mb-14 sm:pt-6 sm:px-7">
<Card className="pt-5 px-4 pb-8 min-h-96 sm:pt-6 sm:px-7">
<CardHeader className="p-0">
<CardTitle className="mb-1">
<Trans>Settings</Trans>
@@ -132,14 +120,14 @@ function SettingsContent({ name }: { name: string }) {
switch (name) {
case "general":
return <GeneralSettings userSettings={userSettings} />
return <General userSettings={userSettings} />
case "notifications":
return <NotificationsSettings userSettings={userSettings} />
return <Notifications userSettings={userSettings} />
case "config":
return <ConfigYamlSettings />
return <ConfigYaml />
case "tokens":
return <FingerprintsSettings />
return <Fingerprints />
case "alert-history":
return <AlertsHistoryDataTableSettings />
return <AlertsHistoryDataTable />
}
}

View File

@@ -3,6 +3,7 @@ import { Trans } from "@lingui/react/macro"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { pb } from "@/lib/stores"
import { Separator } from "@/components/ui/separator"
import { Card } from "@/components/ui/card"
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
@@ -12,8 +13,8 @@ import { InputTags } from "@/components/ui/input-tags"
import { UserSettings } from "@/types"
import { saveSettings } from "./layout"
import * as v from "valibot"
import { isAdmin } from "@/lib/utils"
import { prependBasePath } from "@/components/router"
import { isAdmin, pb } from "@/lib/api"
interface ShoutrrrUrlCardProps {
url: string

View File

@@ -1,6 +1,5 @@
import React from "react"
import { cn } from "@/lib/utils"
import { isAdmin, isReadOnlyUser } from "@/lib/api"
import { cn, isAdmin, isReadOnlyUser } from "@/lib/utils"
import { buttonVariants } from "../../ui/button"
import { $router, Link, navigate } from "../../router"
import { useStore } from "@nanostores/react"
@@ -14,7 +13,6 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
icon?: React.FC<React.SVGProps<SVGSVGElement>>
admin?: boolean
noReadOnly?: boolean
preload?: () => Promise<{ default: React.ComponentType<any> }>
}[]
}
@@ -54,7 +52,6 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
}
return (
<Link
onMouseEnter={() => item.preload?.()}
key={item.href}
href={item.href}
className={cn(

View File

@@ -1,6 +1,6 @@
import { Trans, useLingui } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { $publicKey } from "@/lib/stores"
import { $publicKey, pb } from "@/lib/stores"
import { memo, useEffect, useMemo, useState } from "react"
import { Table, TableCell, TableHead, TableBody, TableRow, TableHeader } from "@/components/ui/table"
import { FingerprintRecord } from "@/types"
@@ -14,8 +14,7 @@ import {
Trash2Icon,
} from "lucide-react"
import { toast } from "@/components/ui/use-toast"
import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils"
import { isReadOnlyUser, pb } from "@/lib/api"
import { cn, copyToClipboard, generateToken, getHubURL, isReadOnlyUser, tokenMap } from "@/lib/utils"
import {
DropdownMenu,
DropdownMenuContent,
@@ -272,7 +271,7 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
<div className="rounded-md border overflow-hidden w-full mt-4">
<Table>
<TableHeader>
<tr className="border-border/50">
<TableRow>
{headerCols.map((col) => (
<TableHead key={col.label} style={{ minWidth: col.w }}>
<span className="flex items-center gap-2">
@@ -288,14 +287,12 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
</span>
</TableHead>
)}
</tr>
</TableRow>
</TableHeader>
<TableBody className="whitespace-pre">
{fingerprints.map((fingerprint, i) => (
<TableRow key={i}>
<TableCell className="font-medium ps-5 py-2 max-w-60 truncate">
{fingerprint.expand.system.name}
</TableCell>
<TableCell className="font-medium ps-5 py-2">{fingerprint.expand.system.name}</TableCell>
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.token}</TableCell>
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.fingerprint}</TableCell>
{!isReadOnly && (

View File

@@ -2,18 +2,17 @@ import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro"
import {
$systems,
pb,
$chartTime,
$containerFilter,
$userSettings,
$direction,
$maxValues,
$temperatureFilter,
$allSystemsByName,
} from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import { useContainerChartConfigs } from "@/components/charts/hooks"
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
import { useStore } from "@nanostores/react"
import Spinner from "../spinner"
@@ -25,12 +24,12 @@ import {
decimalString,
formatBytes,
getHostDisplayValue,
getPbTimestamp,
listen,
parseSemVer,
toFixedFloat,
useBrowserStorage,
useLocalStorage,
} from "@/lib/utils"
import { getPbTimestamp, pb } from "@/lib/api"
import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import { Button } from "../ui/button"
@@ -43,15 +42,15 @@ import { useLingui } from "@lingui/react/macro"
import { $router, navigate } from "../router"
import { getPagePath } from "@nanostores/router"
import { batteryStateTranslations } from "@/lib/i18n"
import AreaChartDefault from "@/components/charts/area-chart"
import ContainerChart from "@/components/charts/container-chart"
import MemChart from "@/components/charts/mem-chart"
import DiskChart from "@/components/charts/disk-chart"
import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart"
import GpuPowerChart from "@/components/charts/gpu-power-chart"
import LoadAverageChart from "@/components/charts/load-average-chart"
import { subscribeKeys } from "nanostores"
const AreaChartDefault = lazy(() => import("../charts/area-chart"))
const ContainerChart = lazy(() => import("../charts/container-chart"))
const MemChart = lazy(() => import("../charts/mem-chart"))
const DiskChart = lazy(() => import("../charts/disk-chart"))
const SwapChart = lazy(() => import("../charts/swap-chart"))
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
const GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
const LoadAverageChart = lazy(() => import("../charts/load-average-chart"))
const cache = new Map<string, any>()
@@ -120,13 +119,13 @@ function dockerOrPodman(str: string, system: SystemRecord) {
return str
}
export default memo(function SystemDetail({ name }: { name: string }) {
export default function SystemDetail({ name }: { name: string }) {
const direction = useStore($direction)
const { t } = useLingui()
const systems = useStore($systems)
const chartTime = useStore($chartTime)
const maxValues = useStore($maxValues)
const [grid, setGrid] = useBrowserStorage("grid", true)
const [grid, setGrid] = useLocalStorage("grid", true)
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
@@ -152,13 +151,36 @@ export default memo(function SystemDetail({ name }: { name: string }) {
}
}, [name])
// find matching system and update when it changes
// function resetCharts() {
// setSystemStats([])
// setContainerData([])
// }
// useEffect(resetCharts, [chartTime])
// find matching system
useEffect(() => {
return subscribeKeys($allSystemsByName, [name], (newSystems) => {
const sys = newSystems[name]
sys?.id && setSystem(sys)
if (system.id && system.name === name) {
return
}
const matchingSystem = systems.find((s) => s.name === name) as SystemRecord
if (matchingSystem) {
setSystem(matchingSystem)
}
}, [name, system, systems])
// update system when new data is available
useEffect(() => {
if (!system.id) {
return
}
pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
setSystem(e.record)
})
}, [name])
return () => {
pb.collection("systems").unsubscribe(system.id)
}
}, [system.id])
const chartData: ChartData = useMemo(() => {
const lastCreated = Math.max(
@@ -175,9 +197,6 @@ export default memo(function SystemDetail({ name }: { name: string }) {
}
}, [systemStats, containerData, direction])
// Share chart config computation for all container charts
const containerChartConfigs = useContainerChartConfigs(containerData)
// get stats
useEffect(() => {
if (!system.id || !chartTime) {
@@ -268,19 +287,11 @@ export default memo(function SystemDetail({ name }: { name: string }) {
value: system.info.k,
},
}
let uptime: React.ReactNode
if (system.info.u < 3600) {
uptime = (
<Plural
value={Math.trunc(system.info.u / 60)}
one="# minute"
few="# minutes"
many="# minutes"
other="# minutes"
/>
)
} else if (system.info.u < 172800) {
uptime = <Plural value={Math.trunc(system.info.u / 3600)} one="# hour" other="# hours" />
if (system.info.u < 172800) {
const hours = Math.trunc(system.info.u / 3600)
uptime = <Plural value={hours} one="# hour" other="# hours" />
} else {
uptime = <Plural value={Math.trunc(system.info?.u / 86400)} one="# day" other="# days" />
}
@@ -306,7 +317,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
Icon: any
hide?: boolean
}[]
}, [system.info, t])
}, [system.info])
/** Space for tooltip if more than 12 containers */
useEffect(() => {
@@ -380,7 +391,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
return (
<>
<div id="chartwrap" className="grid gap-4 mb-14 overflow-x-clip">
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
{/* system info */}
<Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
@@ -475,7 +486,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t`CPU Usage`,
dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),
color: 1,
color: "1",
opacity: 0.4,
},
]}
@@ -492,12 +503,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
description={t`Average CPU utilization of containers`}
cornerEl={containerFilterBar}
>
<ContainerChart
chartData={chartData}
dataKey="c"
chartType={ChartType.CPU}
chartConfig={containerChartConfigs.cpu}
/>
<ContainerChart chartData={chartData} dataKey="c" chartType={ChartType.CPU} />
</ChartCard>
)}
@@ -506,9 +512,8 @@ export default memo(function SystemDetail({ name }: { name: string }) {
grid={grid}
title={t`Memory Usage`}
description={t`Precise utilization at the recorded time`}
cornerEl={maxValSelect}
>
<MemChart chartData={chartData} showMax={showMax} />
<MemChart chartData={chartData} />
</ChartCard>
{containerFilterBar && (
@@ -519,12 +524,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
cornerEl={containerFilterBar}
>
<ContainerChart
chartData={chartData}
dataKey="m"
chartType={ChartType.Memory}
chartConfig={containerChartConfigs.memory}
/>
<ContainerChart chartData={chartData} dataKey="m" chartType={ChartType.Memory} />
</ChartCard>
)}
@@ -546,13 +546,13 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
color: 3,
color: "3",
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
color: 1,
color: "1",
opacity: 0.3,
},
]}
@@ -587,7 +587,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
}
return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024
},
color: 5,
color: "5",
opacity: 0.2,
},
{
@@ -598,7 +598,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
}
return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024
},
color: 2,
color: "2",
opacity: 0.2,
},
]}
@@ -626,12 +626,8 @@ export default memo(function SystemDetail({ name }: { name: string }) {
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
cornerEl={containerFilterBar}
>
<ContainerChart
chartData={chartData}
chartType={ChartType.Network}
dataKey="n"
chartConfig={containerChartConfigs.network}
/>
{/* @ts-ignore */}
<ContainerChart chartData={chartData} chartType={ChartType.Network} dataKey="n" />
</ChartCard>
</div>
)}
@@ -691,7 +687,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t`Charge`,
dataKey: ({ stats }) => stats?.bat?.[0],
color: 1,
color: "1",
opacity: 0.35,
},
]}
@@ -734,7 +730,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
color: 1,
color: "1",
opacity: 0.35,
},
]}
@@ -754,7 +750,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
color: 2,
color: "2",
opacity: 0.25,
},
]}
@@ -806,13 +802,13 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{
label: t`Write`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "wm" : "w"] ?? 0,
color: 3,
color: "3",
opacity: 0.3,
},
{
label: t`Read`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "rm" : "r"] ?? 0,
color: 1,
color: "1",
opacity: 0.3,
},
]}
@@ -838,7 +834,7 @@ export default memo(function SystemDetail({ name }: { name: string }) {
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
</>
)
})
}
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
const containerFilter = useStore(store)

View File

@@ -23,11 +23,12 @@ import {
formatBytes,
formatTemperature,
getMeterState,
isReadOnlyUser,
parseSemVer,
} from "@/lib/utils"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useStore } from "@nanostores/react"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
import { $userSettings, pb } from "@/lib/stores"
import { Trans, useLingui } from "@lingui/react/macro"
import { useMemo, useRef, useState } from "react"
import { memo } from "react"
@@ -56,7 +57,6 @@ import { t } from "@lingui/core/macro"
import { MeterState, SystemStatus } from "@/lib/enums"
import { $router, Link } from "../router"
import { getPagePath } from "@nanostores/router"
import { isReadOnlyUser, pb } from "@/lib/api"
const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500",
@@ -72,8 +72,7 @@ const STATUS_COLORS = {
export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
return [
{
// size: 200,
size: 100,
size: 200,
minSize: 0,
accessorKey: "name",
id: "system",
@@ -112,15 +111,11 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
Icon: ServerIcon,
cell: (info) => {
const { name } = info.row.original
const longestName = useStore($longestSystemNameLen)
return (
<>
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1">
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1 md:pe-5">
<IndicatorDot system={info.row.original} />
{/* NOTE: change to 1 ch if switching to monospace font */}
<span className="truncate" style={{ width: `${longestName / 1.1}ch` }}>
{name}
</span>
{name}
</span>
<Link
href={getPagePath($router, "system", { name })}
@@ -323,18 +318,22 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const val = Number(info.getValue()) || 0
const threshold = getMeterState(val)
const meterClass = cn(
"h-full",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
<span className="min-w-8 shrink-0">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
<span className={meterClass} style={{ width: `${val}%` }}></span>
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span
className={cn(
"absolute inset-0 w-full h-full origin-left",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)}
style={{
transform: `scalex(${val / 100})`,
}}
></span>
</span>
</div>
)

View File

@@ -11,8 +11,11 @@ import {
Row,
Table as TableType,
} from "@tanstack/react-table"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -33,65 +36,33 @@ import {
ArrowUpIcon,
Settings2Icon,
EyeIcon,
FilterIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $pausedSystems, $downSystems, $upSystems, $systems } from "@/lib/stores"
import { memo, useEffect, useMemo, useState } from "react"
import { $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
import { cn, useLocalStorage } from "@/lib/utils"
import { $router, Link } from "../router"
import { useLingui, Trans } from "@lingui/react/macro"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "@/components/ui/input"
import { Input } from "../ui/input"
import { getPagePath } from "@nanostores/router"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
import AlertButton from "../alerts/alert-button"
import { SystemStatus } from "@/lib/enums"
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
type ViewMode = "table" | "grid"
type StatusFilter = "all" | SystemRecord["status"]
const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
export default function SystemsTable() {
const data = useStore($systems)
const downSystems = $downSystems.get()
const upSystems = $upSystems.get()
const pausedSystems = $pausedSystems.get()
const { i18n, t } = useLingui()
const [filter, setFilter] = useState<string>()
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [sorting, setSorting] = useBrowserStorage<SortingState>(
"sortMode",
[{ id: "system", desc: false }],
sessionStorage
)
const [sorting, setSorting] = useState<SortingState>([{ id: "system", desc: false }])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useBrowserStorage<VisibilityState>("cols", {})
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
const locale = i18n.locale
// Filter data based on status filter
const filteredData = useMemo(() => {
if (statusFilter === "all") {
return data
}
if (statusFilter === SystemStatus.Up) {
return Object.values(upSystems) ?? []
}
if (statusFilter === SystemStatus.Down) {
return Object.values(downSystems) ?? []
}
return Object.values(pausedSystems) ?? []
}, [data, statusFilter])
const [viewMode, setViewMode] = useBrowserStorage<ViewMode>(
"viewMode",
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table"
)
useEffect(() => {
if (filter !== undefined) {
table.getColumn("system")?.setFilterValue(filter)
@@ -101,7 +72,7 @@ export default function SystemsTable() {
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode])
const table = useReactTable({
data: filteredData,
data,
columns: columnDefs,
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
@@ -115,6 +86,7 @@ export default function SystemsTable() {
columnVisibility,
},
defaultColumn: {
// sortDescFirst: true,
invertSorting: true,
sortUndefined: "last",
minSize: 0,
@@ -126,25 +98,19 @@ export default function SystemsTable() {
const rows = table.getRowModel().rows
const columns = table.getAllColumns()
const visibleColumns = table.getVisibleLeafColumns()
const [upSystemsLength, downSystemsLength, pausedSystemsLength] = useMemo(() => {
return [Object.values(upSystems).length, Object.values(downSystems).length, Object.values(pausedSystems).length]
}, [upSystems, downSystems, pausedSystems])
// TODO: hiding temp then gpu messes up table headers
const CardHead = useMemo(() => {
return (
<CardHeader className="pb-4.5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="grid md:flex gap-5 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2">
<CardTitle className="mb-2.5">
<Trans>All Systems</Trans>
</CardTitle>
<CardDescription className="flex">
<Trans>Click on a system to view more information.</Trans>
<CardDescription>
<Trans>Updated in real time. Click on a system to view information.</Trans>
</CardDescription>
</div>
<div className="flex gap-2 ms-auto w-full md:w-80">
<Input placeholder={t`Filter...`} onChange={(e) => setFilter(e.target.value)} className="px-4" />
<DropdownMenu>
@@ -155,8 +121,8 @@ export default function SystemsTable() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="h-72 md:h-auto min-w-48 md:min-w-auto overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-4 divide-y md:divide-s md:divide-y-0">
<div className="border-r">
<div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-s md:divide-y-0">
<div>
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<LayoutGridIcon className="size-4" />
<Trans>Layout</Trans>
@@ -178,33 +144,7 @@ export default function SystemsTable() {
</DropdownMenuRadioGroup>
</div>
<div className="border-r">
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<FilterIcon className="size-4" />
<Trans>Status</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
className="px-1 pb-1"
value={statusFilter}
onValueChange={(value) => setStatusFilter(value as StatusFilter)}
>
<DropdownMenuRadioItem value="all" onSelect={(e) => e.preventDefault()}>
<Trans>All Systems</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="up" onSelect={(e) => e.preventDefault()}>
<Trans>Up ({upSystemsLength})</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="down" onSelect={(e) => e.preventDefault()}>
<Trans>Down ({downSystemsLength})</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="paused" onSelect={(e) => e.preventDefault()}>
<Trans>Paused ({pausedSystemsLength})</Trans>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</div>
<div className="border-r">
<div>
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<ArrowUpDownIcon className="size-4" />
<Trans>Sort By</Trans>
@@ -270,16 +210,7 @@ export default function SystemsTable() {
</div>
</CardHeader>
)
}, [
visibleColumns.length,
sorting,
viewMode,
locale,
statusFilter,
upSystemsLength,
downSystemsLength,
pausedSystemsLength,
])
}, [visibleColumns.length, sorting, viewMode, locale])
return (
<Card>
@@ -287,7 +218,7 @@ export default function SystemsTable() {
<div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
{viewMode === "table" ? (
// table layout
<div className="rounded-md">
<div className="rounded-md border overflow-hidden">
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
</div>
) : (
@@ -309,78 +240,36 @@ export default function SystemsTable() {
)
}
const AllSystemsTable = memo(function ({
table,
rows,
colLength,
}: {
table: TableType<SystemRecord>
rows: Row<SystemRecord>[]
colLength: number
}) {
// The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => (rows.length > 10 ? 56 : 60),
getScrollElement: () => scrollRef.current,
overscan: 5,
})
const virtualRows = virtualizer.getVirtualItems()
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full">
<SystemsTableHead table={table} colLength={colLength} />
<TableBody onMouseEnter={preloadSystemDetail}>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<SystemRecord>
return (
<SystemTableRow
key={row.id}
row={row}
virtualRow={virtualRow}
length={rows.length}
colLength={colLength}
/>
)
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</div>
)
})
const AllSystemsTable = memo(
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
return (
<Table>
<SystemsTableHead table={table} colLength={colLength} />
<TableBody>
{rows.length ? (
rows.map((row) => (
<SystemTableRow key={row.original.id} row={row} length={rows.length} colLength={colLength} />
))
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-24 text-center">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
)
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
const { i18n } = useLingui()
return useMemo(() => {
return (
<TableHeader className="sticky top-0 z-20 w-full border-b-2">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-1.5" key={header.id}>
@@ -388,49 +277,41 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
</TableHead>
)
})}
</tr>
</TableRow>
))}
</TableHeader>
)
}, [i18n.locale, colLength])
}
const SystemTableRow = memo(function ({
row,
virtualRow,
colLength,
}: {
row: Row<SystemRecord>
virtualRow: VirtualItem
length: number
colLength: number
}) {
const system = row.original
const { t } = useLingui()
return useMemo(() => {
return (
<TableRow
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
"opacity-50": system.status === SystemStatus.Paused,
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
height: virtualRow.size,
}}
className="py-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}, [system, system.status, colLength, t])
})
const SystemTableRow = memo(
({ row, length, colLength }: { row: Row<SystemRecord>; length: number; colLength: number }) => {
const system = row.original
const { t } = useLingui()
return useMemo(() => {
return (
<TableRow
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative", {
"opacity-50": system.status === SystemStatus.Paused,
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
}}
className={length > 10 ? "py-2" : "py-2.5"}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}, [system, system.status, colLength, t])
}
)
const SystemCard = memo(
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
@@ -440,7 +321,6 @@ const SystemCard = memo(
return useMemo(() => {
return (
<Card
onMouseEnter={preloadSystemDetail}
key={system.id}
className={cn(
"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
@@ -450,11 +330,13 @@ const SystemCard = memo(
)}
>
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
<div className="flex items-center gap-2 w-full overflow-hidden">
<CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5">
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-base tracking-normal shrink-1 text-primary/90 flex items-center min-h-10 gap-2.5 min-w-0">
<div className="flex items-center gap-2.5 min-w-0">
<IndicatorDot system={system} />
<span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span>
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
{system.name}
</CardTitle>
</div>
</CardTitle>
{table.getColumn("actions")?.getIsVisible() && (
@@ -465,33 +347,23 @@ const SystemCard = memo(
)}
</div>
</CardHeader>
<CardContent className="text-sm px-5 pt-3.5 pb-4">
<div className="grid gap-2.5" style={{ gridTemplateColumns: "24px minmax(80px, max-content) 1fr" }}>
{table.getAllColumns().map((column) => {
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
if (!cell) return null
// @ts-ignore
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
return (
<>
<div key={`${column.id}-icon`} className="flex items-center">
{column.id === "lastSeen" ? (
<EyeIcon className="size-4 text-muted-foreground" />
) : (
Icon && <Icon className="size-4 text-muted-foreground" />
)}
</div>
<div key={`${column.id}-label`} className="flex items-center text-muted-foreground pr-3">
{name()}:
</div>
<div key={`${column.id}-value`} className="flex items-center">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</>
)
})}
</div>
<CardContent className="grid gap-2.5 text-sm px-5 pt-3.5 pb-4">
{table.getAllColumns().map((column) => {
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
if (!cell) return null
// @ts-ignore
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
return (
<div key={column.id} className="flex items-center gap-3">
{Icon && <Icon className="size-4 text-muted-foreground" />}
<div className="flex items-center gap-3 flex-1">
<span className="text-muted-foreground min-w-16">{name()}:</span>
<div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
</div>
</div>
)
})}
</CardContent>
<Link
href={getPagePath($router, "system", { name: row.original.name })}

View File

@@ -1,41 +1,33 @@
import * as React from "react"
import { DialogTitle, type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Dialog, DialogContent } from "@/components/ui/dialog"
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn("bg-card flex h-full w-full flex-col overflow-hidden rounded-md", className)}
{...props}
/>
)
}
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn("flex h-full w-full flex-col overflow-hidden bg-card", className)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className={cn("overflow-hidden p-0", className)}>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<DialogContent className="overflow-hidden p-0 shadow-lg">
<div className="sr-only">
<DialogTitle>Command</DialogTitle>
</div>
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@@ -43,81 +35,89 @@ function CommandDialog({
)
}
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="flex h-9 items-center gap-2 border-b px-3">
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
{...props}
/>
)
}
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return <CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm" {...props} />
}
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="me-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
)
}
</div>
))
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
CommandInput.displayName = CommandPrimitive.Input.displayName
function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent/70 data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn("text-muted-foreground ml-auto text-xs tracking-wide", className)}
{...props}
/>
)
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />)
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default opacity-70 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden aria-selected:bg-accent/60 aria-selected:opacity-90 data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ms-auto text-xs tracking-wide text-muted-foreground", className)} {...props} />
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,

View File

@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"cursor-pointer relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
"relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
inset && "ps-8",
className
)}
@@ -95,7 +95,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
"relative flex select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
checked={checked}
@@ -118,7 +118,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...props}

View File

@@ -15,7 +15,7 @@ export function InputCopy({ value, id, name }: { value: string; id: string; name
}
></div>
<TooltipProvider delayDuration={100} disableHoverableContent>
<Tooltip disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"

View File

@@ -2,19 +2,21 @@ import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
data-slot="input"
className={cn(
"flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
ref={ref}
{...props}
/>
)
}
})
Input.displayName = "Input"
export { Input }

View File

@@ -13,11 +13,7 @@ Table.displayName = "Table"
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead
ref={ref}
className={cn("bg-table-header border-b border-border/50 [&_tr]:border-b", className)}
{...props}
/>
<thead ref={ref} className={cn("bg-muted/30 [&_tr]:border-b", className)} {...props} />
)
)
TableHeader.displayName = "TableHeader"

View File

@@ -3,47 +3,26 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
}
const TooltipProvider = TooltipPrimitive.Provider
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
const Tooltip = TooltipPrimitive.Root
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
const TooltipTrigger = TooltipPrimitive.Trigger
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground border animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-sm text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow
className="bg-popover border z-50 fill-popover size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] will-change-transform"
style={{ clipPath: "inset(25% 0 0 25%)" }}
/>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -1,15 +1,13 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant light (&:is(.light *));
@custom-variant dark (&:is(.dark *));
@custom-variant safari (@supports (hanging-punctuation: first) and (-webkit-appearance: none));
:root {
--background: hsl(30 8% 98%);
--foreground: hsl(30 0% 10%);
--foreground: hsl(30 0% 0%);
--card: hsl(30 0% 100%);
--card-foreground: hsl(240 6% 12%);
--card-foreground: hsl(240 6.67% 2.94%);
--popover: hsl(30 0% 100%);
--popover-foreground: hsl(240 10% 6.2%);
--primary: hsl(240 5.88% 10%);
@@ -21,7 +19,7 @@
--accent: hsl(20 23.08% 94%);
--accent-foreground: hsl(240 5.88% 10%);
--destructive: hsl(0 66% 53%);
--destructive-foreground: hsl(0 0% 97%);
--destructive-foreground: hsl(0 0% 98.04%);
--border: hsl(30 8.11% 85.49%);
--input: hsl(30 4.29% 72.55%);
--ring: hsl(30 3.97% 49.41%);
@@ -31,7 +29,6 @@
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--table-header: hsl(225, 6%, 97%);
}
.dark {
@@ -51,10 +48,10 @@
--accent: hsl(220 5% 15.5%);
--accent-foreground: hsl(220 2% 98%);
--destructive: hsl(0 62% 46%);
--destructive-foreground: hsl(0 0% 97%);
--border: hsl(220 3% 16%);
--input: hsl(220 4% 22%);
--ring: hsl(220 4% 80%);
--table-header: hsl(220, 6%, 13%);
--radius: 0.8rem;
}
@@ -97,7 +94,6 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -106,7 +102,6 @@
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-table-header: var(--table-header);
}
@layer utilities {

View File

@@ -1,10 +1,9 @@
import type { AlertInfo, AlertRecord } from "@/types"
import type { RecordSubscription } from "pocketbase"
import { $alerts } from "@/lib/stores"
import { pb, $alerts } from "@/lib/stores"
import { EthernetIcon } from "@/components/ui/icons"
import { ServerIcon, CpuIcon, MemoryStickIcon, HardDriveIcon, ThermometerIcon, HourglassIcon } from "lucide-react"
import { t } from "@lingui/core/macro"
import { pb } from "./api"
/** Alert info for each alert type */
export const alertInfo: Record<string, AlertInfo> = {

View File

@@ -1,66 +0,0 @@
import { ChartTimes, UserSettings } from "@/types"
import { $alerts, $allSystemsByName, $userSettings } from "./stores"
import { toast } from "@/components/ui/use-toast"
import { t } from "@lingui/core/macro"
import { chartTimeData } from "./utils"
import PocketBase from "pocketbase"
import { basePath } from "@/components/router"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
export const isAdmin = () => pb.authStore.record?.role === "admin"
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
export const verifyAuth = () => {
pb.collection("users")
.authRefresh()
.catch(() => {
logOut()
toast({
title: t`Failed to authenticate`,
description: t`Please log in again`,
variant: "destructive",
})
})
}
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
export async function logOut() {
$allSystemsByName.set({})
$alerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
pb.realtime.unsubscribe()
}
/** Fetch or create user settings in database */
export async function updateUserSettings() {
try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings)
return
} catch (e) {
console.error("get settings", e)
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)
}
}
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

View File

@@ -1,23 +1,17 @@
import { atom, computed, map, ReadableAtom } from "nanostores"
import PocketBase from "pocketbase"
import { atom, map } from "nanostores"
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
import { basePath } from "@/components/router"
import { Unit } from "./enums"
import { pb } from "./api"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
/** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid)
/** Map of system records by name */
export const $allSystemsByName = map<Record<string, SystemRecord>>({})
/** Map of system records by id */
export const $allSystemsById = map<Record<string, SystemRecord>>({})
/** Map of up systems by id */
export const $upSystems = map<Record<string, SystemRecord>>({})
/** Map of down systems by id */
export const $downSystems = map<Record<string, SystemRecord>>({})
/** Map of paused systems by id */
export const $pausedSystems = map<Record<string, SystemRecord>>({})
/** List of all system records */
export const $systems: ReadableAtom<SystemRecord[]> = computed($allSystemsById, Object.values)
/** List of system records */
export const $systems = atom<SystemRecord[]>([])
/** Map of alert records by system id and alert name */
export const $alerts = map<AlertMap>({})
@@ -63,8 +57,3 @@ export const $copyContent = atom("")
/** Direction for localization */
export const $direction = atom<"ltr" | "rtl">("ltr")
/** Longest system name length. Used to set table column width. I know this
* is stupid but the table is virtualized and I know this will work.
*/
export const $longestSystemNameLen = atom(8)

View File

@@ -1,174 +0,0 @@
import { SystemRecord } from "@/types"
import { PreinitializedMapStore } from "nanostores"
import { pb, verifyAuth } from "@/lib/api"
import {
$allSystemsByName,
$upSystems,
$downSystems,
$pausedSystems,
$allSystemsById,
$longestSystemNameLen,
} from "@/lib/stores"
import { updateFavicon, FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED } from "@/lib/utils"
import { SystemStatus } from "./enums"
const COLLECTION = pb.collection<SystemRecord>("systems")
const FIELDS_DEFAULT = "id,name,host,port,info,status"
/** Maximum system name length for display purposes */
const MAX_SYSTEM_NAME_LENGTH = 20
let initialized = false
let unsub: (() => void) | undefined | void
/** Initialize the systems manager and set up listeners */
export function init() {
if (initialized) {
return
}
initialized = true
// sync system stores on change
$allSystemsById.listen((newSystems, oldSystems, changedKey) => {
const oldSystem = oldSystems[changedKey]
const newSystem = newSystems[changedKey]
// if system is undefined (deleted), remove it from the stores
if (oldSystem && !newSystem?.id) {
removeFromStore(oldSystem, $upSystems)
removeFromStore(oldSystem, $downSystems)
removeFromStore(oldSystem, $pausedSystems)
removeFromStore(oldSystem, $allSystemsById)
}
if (!newSystem) {
onSystemsChanged(newSystems, undefined)
return
}
const newStatus = newSystem.status
if (newStatus === SystemStatus.Up) {
$upSystems.setKey(newSystem.id, newSystem)
removeFromStore(newSystem, $downSystems)
removeFromStore(newSystem, $pausedSystems)
} else if (newStatus === SystemStatus.Down) {
$downSystems.setKey(newSystem.id, newSystem)
removeFromStore(newSystem, $upSystems)
removeFromStore(newSystem, $pausedSystems)
} else if (newStatus === SystemStatus.Paused) {
$pausedSystems.setKey(newSystem.id, newSystem)
removeFromStore(newSystem, $upSystems)
removeFromStore(newSystem, $downSystems)
} else if (newStatus === SystemStatus.Pending) {
removeFromStore(newSystem, $upSystems)
removeFromStore(newSystem, $downSystems)
removeFromStore(newSystem, $pausedSystems)
}
// run things that need to be done when systems change
onSystemsChanged(newSystems, newSystem)
})
}
/** Update the longest system name length and favicon based on system status */
function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: SystemRecord | undefined) {
const upSystemsStore = $upSystems.get()
const downSystemsStore = $downSystems.get()
const upSystems = Object.values(upSystemsStore)
const downSystems = Object.values(downSystemsStore)
// Update longest system name length
const longestName = $longestSystemNameLen.get()
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, changedSystem?.name.length || 0)
if (nameLen > longestName) {
$longestSystemNameLen.set(nameLen)
}
// Update favicon based on system status
if (downSystems.length > 0) {
updateFavicon(FAVICON_RED)
} else if (upSystems.length > 0) {
updateFavicon(FAVICON_GREEN)
} else {
updateFavicon(FAVICON_DEFAULT)
}
}
/** Fetch systems from collection */
async function fetchSystems(): Promise<SystemRecord[]> {
try {
return await COLLECTION.getFullList({ sort: "+name", fields: FIELDS_DEFAULT })
} catch (error) {
console.error("Failed to fetch systems:", error)
return []
}
}
/** Add system to both name and ID stores */
export function add(system: SystemRecord) {
$allSystemsByName.setKey(system.name, system)
$allSystemsById.setKey(system.id, system)
}
/** Update system in stores */
export function update(system: SystemRecord) {
// if name changed, make sure old name is removed from the name store
const oldName = $allSystemsById.get()[system.id]?.name
if (oldName !== system.name) {
$allSystemsByName.setKey(oldName, undefined as any)
}
add(system)
}
/** Remove system from stores */
export function remove(system: SystemRecord) {
removeFromStore(system, $allSystemsByName)
removeFromStore(system, $allSystemsById)
removeFromStore(system, $upSystems)
removeFromStore(system, $downSystems)
removeFromStore(system, $pausedSystems)
}
/** Remove system from specific store */
function removeFromStore(system: SystemRecord, store: PreinitializedMapStore<Record<string, SystemRecord>>) {
const key = store === $allSystemsByName ? system.name : system.id
store.setKey(key, undefined as any)
}
/** Action functions for subscription */
const actionFns: Record<string, (system: SystemRecord) => void> = {
create: add,
update: update,
delete: remove,
}
/** Subscribe to real-time system updates from the collection */
export async function subscribe() {
try {
unsub = await COLLECTION.subscribe("*", ({ action, record }) => actionFns[action]?.(record), {
fields: FIELDS_DEFAULT,
})
} catch (error) {
console.error("Failed to subscribe to systems collection:", error)
}
}
/** Refresh all systems with latest data from the hub */
export async function refresh() {
try {
const records = await fetchSystems()
if (!records.length) {
// No systems found, verify authentication
verifyAuth()
return
}
for (const record of records) {
add(record)
}
} catch (error) {
console.error("Failed to refresh systems:", error)
}
}
/** Unsubscribe from real-time system updates */
export const unsubscribe = () => (unsub = unsub?.())

View File

@@ -2,16 +2,14 @@ import { t } from "@lingui/core/macro"
import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { $copyContent, $userSettings } from "./stores"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
import type { ChartTimeData, ChartTimes, FingerprintRecord, SemVer, SystemRecord, UserSettings } from "@/types"
import { RecordModel, RecordSubscription } from "pocketbase"
import { WritableAtom } from "nanostores"
import { timeDay, timeHour } from "d3-time"
import { useEffect, useState } from "react"
import { MeterState, Unit } from "./enums"
import { prependBasePath } from "@/components/router"
export const FAVICON_DEFAULT = "favicon.svg"
export const FAVICON_GREEN = "favicon-green.svg"
export const FAVICON_RED = "favicon-red.svg"
import { MeterState, Unit } from "./enums"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -36,6 +34,52 @@ export async function copyToClipboard(content: string) {
}
}
const verifyAuth = () => {
pb.collection("users")
.authRefresh()
.catch(() => {
logOut()
toast({
title: t`Failed to authenticate`,
description: t`Please log in again`,
variant: "destructive",
})
})
}
export const updateSystemList = (() => {
let isFetchingSystems = false
return async () => {
if (isFetchingSystems) {
return
}
isFetchingSystems = true
try {
const records = await pb
.collection<SystemRecord>("systems")
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
if (records.length) {
$systems.set(records)
} else {
verifyAuth()
}
} finally {
isFetchingSystems = false
}
}
})()
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
export async function logOut() {
$systems.set([])
$alerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
pb.realtime.unsubscribe()
}
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
hour: "numeric",
minute: "numeric",
@@ -66,6 +110,47 @@ export const updateFavicon = (newIcon: string) => {
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = prependBasePath(`/static/${newIcon}`)
}
export const isAdmin = () => pb.authStore.record?.role === "admin"
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
/** Update systems / alerts list when records change */
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
const curRecords = $store.get()
const newRecords = []
if (e.action === "delete") {
for (const server of curRecords) {
if (server.id !== e.record.id) {
newRecords.push(server)
}
}
} else {
let found = 0
for (const server of curRecords) {
if (server.id === e.record.id) {
found = newRecords.push(e.record)
} else {
newRecords.push(server)
}
}
if (!found) {
newRecords.push(e.record)
}
}
$store.set(newRecords)
}
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export const chartTimeData: ChartTimeData = {
"1h": {
type: "1m",
@@ -108,6 +193,32 @@ export const chartTimeData: ChartTimeData = {
},
}
/** Sets the correct width of the y axis in recharts based on the longest label */
export function useYAxisWidth() {
const [yAxisWidth, setYAxisWidth] = useState(0)
let maxChars = 0
let timeout: Timer
function updateYAxisWidth(str: string) {
if (str.length > maxChars) {
maxChars = str.length
const div = document.createElement("div")
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
div.innerHTML = str
clearTimeout(timeout)
timeout = setTimeout(() => {
document.body.appendChild(div)
const width = div.offsetWidth + 24
if (width > yAxisWidth) {
setYAxisWidth(div.offsetWidth + 24)
}
document.body.removeChild(div)
})
}
return str
}
return { yAxisWidth, updateYAxisWidth }
}
/** Format number to x decimal places, without trailing zeros */
export function toFixedFloat(num: number, digits: number) {
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
@@ -130,20 +241,20 @@ export function decimalString(num: number, digits = 2) {
return formatter.format(num)
}
/** Get value from local or session storage */
function getStorageValue(key: string, defaultValue: any, storageInterface: Storage = localStorage) {
const saved = storageInterface?.getItem(key)
/** Get value from local storage */
function getStorageValue(key: string, defaultValue: any) {
const saved = localStorage?.getItem(key)
return saved ? JSON.parse(saved) : defaultValue
}
/** Hook to sync value in local or session storage */
export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {
/** Hook to sync value in local storage */
export function useLocalStorage<T>(key: string, defaultValue: T) {
key = `besz-${key}`
const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue, storageInterface)
return getStorageValue(key, defaultValue)
})
useEffect(() => {
storageInterface?.setItem(key, JSON.stringify(value))
localStorage?.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
@@ -218,6 +329,24 @@ export function formatBytes(
}
}
/** Fetch or create user settings in database */
export async function updateUserSettings() {
try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings)
return
} catch (e) {
console.error("get settings", e)
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)
}
}
export const chartMargin = { top: 12 }
/**
@@ -228,21 +357,6 @@ export const chartMargin = { top: 12 }
*/
export const getHostDisplayValue = (system: SystemRecord): string => system.host.slice(system.host.lastIndexOf("/") + 1)
// export function formatUptimeString(uptimeSeconds: number): string {
// if (!uptimeSeconds || isNaN(uptimeSeconds)) return ""
// if (uptimeSeconds < 3600) {
// const minutes = Math.trunc(uptimeSeconds / 60)
// return plural({ minutes }, { one: "# minute", other: "# minutes" })
// } else if (uptimeSeconds < 172800) {
// const hours = Math.trunc(uptimeSeconds / 3600)
// console.log(hours)
// return plural({ hours }, { one: "# hour", other: "# hours" })
// } else {
// const days = Math.trunc(uptimeSeconds / 86400)
// return plural({ days }, { one: "# day", other: "# days" })
// }
// }
/** Generate a random token for the agent */
export const generateToken = () => {
try {
@@ -322,20 +436,15 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
}
}
// Cache for runOnce
const runOnceCache = new WeakMap<Function, { done: boolean; result: unknown }>()
/** Run a function only once */
export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: Parameters<T>) => {
let state = runOnceCache.get(fn)
if (!state) {
state = { done: false, result: undefined }
runOnceCache.set(fn, state)
/* returns the name of a system from its id */
export const getSystemNameFromId = (() => {
const cache = new Map<string, string>()
return (systemId: string): string => {
if (cache.has(systemId)) {
return cache.get(systemId)!
}
if (!state.done) {
state.result = fn(...args)
state.done = true
}
return state.result
}) as T
}
const sysName = $systems.get().find((s) => s.id === systemId)?.name ?? ""
cache.set(systemId, sysName)
return sysName
}
})()

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# يوم} other {# أيام}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ساعة} other {# ساعات}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# دقيقة} few {# دقائق} many {# دقيقة} other {# دقيقة}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "تم تحديد {0} من {1} صف"
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعة} other {# ساعات}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 ساعة"
@@ -131,7 +125,6 @@ msgstr "التنبيهات"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "جميع الأنظمة"
@@ -259,10 +252,6 @@ msgstr "تحقق من السجلات لمزيد من التفاصيل."
msgid "Check your notification service"
msgstr "تحقق من خدمة الإشعارات الخاصة بك"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "انقر على نظام لعرض مزيد من المعلومات."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "انقر للنسخ"
@@ -437,10 +426,6 @@ msgstr "التوثيق"
msgid "Down"
msgstr "معطل"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "معطل ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "المدة"
@@ -507,7 +492,7 @@ msgstr "تصدير تكوين الأنظمة الحالية الخاصة بك."
msgid "Fahrenheit (°F)"
msgstr "فهرنهايت (°ف)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "فشل في المصادقة"
@@ -768,10 +753,6 @@ msgstr "إيقاف مؤقت"
msgid "Paused"
msgstr "متوقف مؤقتا"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "متوقف مؤقتا ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "يرجى <0>تكوين خادم SMTP</0> لضمان تسليم التنبيهات."
@@ -793,7 +774,7 @@ msgstr "يرجى إنشاء حساب مسؤول"
msgid "Please enable pop-ups for this site"
msgstr "يرجى تمكين النوافذ المنبثقة لهذا الموقع"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "يرجى تسجيل الدخول مرة أخرى"
@@ -920,7 +901,6 @@ msgstr "الترتيب حسب"
msgid "State"
msgstr "الحالة"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "الحالة"
@@ -1075,7 +1055,6 @@ msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص
msgid "Unit preferences"
msgstr "تفضيلات الوحدة"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "رمز مميز عالمي"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "قيد التشغيل"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "قيد التشغيل ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "محدث في الوقت الحقيقي. انقر على نظام لعرض المعلومات."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "تكوين YAML"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# ден} other {# дни}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# час} other {# часа}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# минута} few {# минути} many {# минути} other {# минути}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} от {1} селектирани."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# час} other {# часа}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 час"
@@ -131,7 +125,6 @@ msgstr "Тревоги"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Всички системи"
@@ -259,10 +252,6 @@ msgstr "Провери log-овете за повече информация."
msgid "Check your notification service"
msgstr "Провери услугата си за удостоверяване"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Кликнете върху система, за да видите повече информация."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Настисни за да копираш"
@@ -437,10 +426,6 @@ msgstr "Документация"
msgid "Down"
msgstr "Офлайн"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Офлайн ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Продължителност"
@@ -507,7 +492,7 @@ msgstr "Експортирай конфигурацията на системи
msgid "Fahrenheit (°F)"
msgstr "Фаренхайт (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Неуспешно удостоверяване"
@@ -768,10 +753,6 @@ msgstr "Пауза"
msgid "Paused"
msgstr "На пауза"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "На пауза ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Моля <0>конфигурурай SMTP сървър</0> за да се подсигуриш, че тревогите са доставени."
@@ -793,7 +774,7 @@ msgstr "Моля създай администраторски акаунт"
msgid "Please enable pop-ups for this site"
msgstr "Моля активирай изскачащите прозорци за този сайт"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Моля влез отново"
@@ -920,7 +901,6 @@ msgstr "Сортиране по"
msgid "State"
msgstr "Състояние"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Статус"
@@ -1075,7 +1055,6 @@ msgstr "Задейства се, когато употребата на няко
msgid "Unit preferences"
msgstr "Предпочитания на единицата"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Универсален тоукън"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Нагоре"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Нагоре ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "Актуализира се в реално време. Натисни на система за да видиш информация."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML конфигурация"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Настройките за потребителя ти са обновени."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# den} few {# dny} other {# dní}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} z {1} vybraných řádků."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hodina"
@@ -131,7 +125,6 @@ msgstr "Výstrahy"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Všechny systémy"
@@ -259,10 +252,6 @@ msgstr "Pro více informací zkontrolujte logy."
msgid "Check your notification service"
msgstr "Zkontrolujte službu upozornění"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Klikněte na systém pro zobrazení více informací."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Klikněte pro zkopírování"
@@ -437,10 +426,6 @@ msgstr "Dokumentace"
msgid "Down"
msgstr "Nefunkční"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Nefunkční ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Doba trvání"
@@ -507,7 +492,7 @@ msgstr "Exportovat aktuální konfiguraci systémů."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheita (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Ověření se nezdařilo"
@@ -768,10 +753,6 @@ msgstr "Pozastavit"
msgid "Paused"
msgstr "Pozastaveno"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pozastaveno ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "<0>nakonfigurujte SMTP server</0> pro zajištění toho, aby byla upozornění doručena."
@@ -793,7 +774,7 @@ msgstr "Vytvořte si prosím účet administrátora"
msgid "Please enable pop-ups for this site"
msgstr "Prosím povolte vyskakovací okna pro tento web"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Přihlaste se prosím znovu"
@@ -920,7 +901,6 @@ msgstr "Seřadit podle"
msgid "State"
msgstr "Stav"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Stav"
@@ -1075,7 +1055,6 @@ msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
msgid "Unit preferences"
msgstr "Předvolby jednotek"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Univerzální token"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Funkční"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Funkční ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "Aktualizováno v reálném čase. Klepnutím na systém zobrazíte informace."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML konfigurace"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Vaše uživatelská nastavení byla aktualizována."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# day} other {# days}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hour} other {# hours}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hour} other {# hours}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 time"
@@ -131,7 +125,6 @@ msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle systemer"
@@ -259,10 +252,6 @@ msgstr "Tjek logfiler for flere detaljer."
msgid "Check your notification service"
msgstr "Tjek din notifikationstjeneste"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Klik for at kopiere"
@@ -437,10 +426,6 @@ msgstr "Dokumentation"
msgid "Down"
msgstr "Nede"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -507,7 +492,7 @@ msgstr "Eksporter din nuværende systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Kunne ikke godkende"
@@ -768,10 +753,6 @@ msgstr "Pause"
msgid "Paused"
msgstr "Sat på pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Konfigurer <0>en SMTP server</0> for at sikre at alarmer bliver leveret."
@@ -793,7 +774,7 @@ msgstr "Opret venligst en administratorkonto"
msgid "Please enable pop-ups for this site"
msgstr "Aktiver pop-ups for dette websted"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Log venligst ind igen"
@@ -920,7 +901,6 @@ msgstr "Sorter efter"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1075,7 +1055,6 @@ msgstr "Udløser når brugen af en disk overstiger en tærskel"
msgid "Unit preferences"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Oppe"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgid "Updated in real time. Click on a system to view information."
msgstr "Opdateret i realtid. Klik på et system for at se information."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML Konfiguration"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Dine brugerindstillinger er opdateret."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# Tag} other {# Tage}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# Stunde} other {# Stunden}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# Minute} other {# Minuten}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} von {1} Zeile(n) ausgewählt."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Stunde} other {# Stunden}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 Stunde"
@@ -131,7 +125,6 @@ msgstr "Warnungen"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle Systeme"
@@ -259,10 +252,6 @@ msgstr "Überprüfe die Protokolle für weitere Details."
msgid "Check your notification service"
msgstr "Überprüfe deinen Benachrichtigungsdienst"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Klicke auf ein System, um weitere Informationen zu sehen."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Zum Kopieren klicken"
@@ -437,10 +426,6 @@ msgstr "Dokumentation"
msgid "Down"
msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Dauer"
@@ -507,7 +492,7 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Authentifizierung fehlgeschlagen"
@@ -616,7 +601,7 @@ msgstr "Durchschnittliche Systemlast 5 Min"
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg"
msgstr "Systemlast"
msgstr "Durchschnittliche Last"
#: src/components/navbar.tsx
msgid "Log Out"
@@ -768,10 +753,6 @@ msgstr "Pause"
msgid "Paused"
msgstr "Pausiert"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausiert ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Bitte <0>konfiguriere einen SMTP-Server</0>, um sicherzustellen, dass Warnungen zugestellt werden."
@@ -793,7 +774,7 @@ msgstr "Bitte erstelle ein Administratorkonto"
msgid "Please enable pop-ups for this site"
msgstr "Bitte aktiviere Pop-ups für diese Seite"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Bitte melde dich erneut an"
@@ -920,7 +901,6 @@ msgstr "Sortieren nach"
msgid "State"
msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1075,7 +1055,6 @@ msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert übersc
msgid "Unit preferences"
msgstr "Einheiten"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Universeller Token"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "aktiv"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "aktiv ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "In Echtzeit aktualisiert. Klicke auf ein System, um Informationen anzuzeigen."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML-Konfiguration"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Deine Benutzereinstellungen wurden aktualisiert."

File diff suppressed because it is too large Load Diff

View File

@@ -18,22 +18,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# day} other {# days}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hour} other {# hours}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} of {1} row(s) selected."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hour} other {# hours}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hour"
@@ -126,7 +120,6 @@ msgstr "Alerts"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "All Systems"
@@ -254,10 +247,6 @@ msgstr "Check logs for more details."
msgid "Check your notification service"
msgstr "Check your notification service"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Click on a system to view more information."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Click to copy"
@@ -432,10 +421,6 @@ msgstr "Documentation"
msgid "Down"
msgstr "Down"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Down ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duration"
@@ -502,7 +487,7 @@ msgstr "Export your current systems configuration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Failed to authenticate"
@@ -763,10 +748,6 @@ msgstr "Pause"
msgid "Paused"
msgstr "Paused"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Paused ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -788,7 +769,7 @@ msgstr "Please create an admin account"
msgid "Please enable pop-ups for this site"
msgstr "Please enable pop-ups for this site"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Please log in again"
@@ -915,7 +896,6 @@ msgstr "Sort By"
msgid "State"
msgstr "State"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1070,7 +1050,6 @@ msgstr "Triggers when usage of any disk exceeds a threshold"
msgid "Unit preferences"
msgstr "Unit preferences"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Universal token"
@@ -1087,8 +1066,8 @@ msgid "Up"
msgstr "Up"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Up ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "Updated in real time. Click on a system to view information."
#: src/components/routes/system.tsx
msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# día} other {# días}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hora} other {# horas}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuto} other {# minutos}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} de {1} fila(s) seleccionada(s)."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hora} other {# horas}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hora"
@@ -46,7 +40,7 @@ msgstr "1 hora"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr "1 min"
msgstr ""
#: src/lib/utils.ts
msgid "1 week"
@@ -59,7 +53,7 @@ msgstr "12 horas"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr "15 min"
msgstr ""
#: src/lib/utils.ts
msgid "24 hours"
@@ -72,7 +66,7 @@ msgstr "30 días"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "5 min"
msgstr "5 min"
msgstr ""
#. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx
@@ -131,7 +125,6 @@ msgstr "Alertas"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Todos los Sistemas"
@@ -202,12 +195,12 @@ msgstr "Binario"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bits (Kbps, Mbps, Gbps)"
msgstr ""
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)"
msgstr ""
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
@@ -224,7 +217,7 @@ msgstr "Precaución - posible pérdida de datos"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr "Celsius (°C)"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
@@ -259,10 +252,6 @@ msgstr "Revise los registros para más detalles."
msgid "Check your notification service"
msgstr "Verifique su servicio de notificaciones"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Haga clic en un sistema para ver más información."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Haga clic para copiar"
@@ -437,10 +426,6 @@ msgstr "Documentación"
msgid "Down"
msgstr "Abajo"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Abajo ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duración"
@@ -505,9 +490,9 @@ msgstr "Exporte la configuración actual de sus sistemas."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Error al autenticar"
@@ -768,10 +753,6 @@ msgstr "Pausar"
msgid "Paused"
msgstr "Pausado"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausado ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Por favor, <0>configure un servidor SMTP</0> para asegurar que las alertas sean entregadas."
@@ -793,7 +774,7 @@ msgstr "Por favor, cree una cuenta de administrador"
msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite las ventanas emergentes para este sitio"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Por favor, inicie sesión de nuevo"
@@ -920,7 +901,6 @@ msgstr "Ordenar por"
msgid "State"
msgstr "Estado"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Estado"
@@ -1018,7 +998,7 @@ msgstr "Alternar tema"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1075,7 +1055,6 @@ msgstr "Se activa cuando el uso de cualquier disco supera un umbral"
msgid "Unit preferences"
msgstr "Preferencias de unidad"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Token universal"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Activo"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Activo ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "Actualizado en tiempo real. Haga clic en un sistema para ver la información."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "Configuración YAML"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Su configuración de usuario ha sido actualizada."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# روز} other {# روز}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ساعت} other {# ساعت}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# دقیقه} few {# دقیقه} many {# دقیقه} other {# دقیقه}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} از {1} ردیف انتخاب شده است."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعت} other {# ساعت}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "۱ ساعت"
@@ -131,7 +125,6 @@ msgstr "هشدارها"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "همه سیستم‌ها"
@@ -259,10 +252,6 @@ msgstr "برای جزئیات بیشتر، لاگ‌ها را بررسی کنی
msgid "Check your notification service"
msgstr "سرویس اطلاع‌رسانی خود را بررسی کنید"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "برای مشاهده اطلاعات بیشتر روی یک سیستم کلیک کنید."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "برای کپی کردن کلیک کنید"
@@ -437,10 +426,6 @@ msgstr "مستندات"
msgid "Down"
msgstr "قطع"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "قطع ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "مدت زمان"
@@ -507,7 +492,7 @@ msgstr "پیکربندی سیستم‌های فعلی خود را خارج کن
msgid "Fahrenheit (°F)"
msgstr "فارنهایت (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "احراز هویت ناموفق بود"
@@ -768,10 +753,6 @@ msgstr "توقف"
msgid "Paused"
msgstr "مکث شده"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "مکث شده ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "لطفاً برای اطمینان از تحویل هشدارها، یک <0>سرور SMTP پیکربندی کنید</0>."
@@ -793,7 +774,7 @@ msgstr "لطفاً یک حساب مدیر ایجاد کنید"
msgid "Please enable pop-ups for this site"
msgstr "لطفاً پنجره‌های بازشو را برای این سایت فعال کنید"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "لطفاً دوباره وارد شوید"
@@ -920,7 +901,6 @@ msgstr "مرتب‌سازی بر اساس"
msgid "State"
msgstr "وضعیت"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "وضعیت"
@@ -1075,7 +1055,6 @@ msgstr "هنگامی که استفاده از هر دیسکی از یک آستا
msgid "Unit preferences"
msgstr "تنظیمات واحدها"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "توکن جهانی"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "فعال"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "فعال ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "به صورت لحظه‌ای به‌روزرسانی می‌شود. برای مشاهده اطلاعات، روی یک سیستم کلیک کنید."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "پیکربندی YAML"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "تنظیمات کاربری شما به‌روزرسانی شد."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# jour} other {# jours}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# heure} other {# heures}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# heure} other {# heures}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 heure"
@@ -131,7 +125,6 @@ msgstr "Alertes"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Tous les systèmes"
@@ -259,10 +252,6 @@ msgstr "Vérifiez les journaux pour plus de détails."
msgid "Check your notification service"
msgstr "Vérifiez votre service de notification"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Cliquez pour copier"
@@ -437,10 +426,6 @@ msgstr "Documentation"
msgid "Down"
msgstr "Injoignable"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -507,7 +492,7 @@ msgstr "Exportez la configuration actuelle de vos systèmes."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Échec de l'authentification"
@@ -768,10 +753,6 @@ msgstr "Pause"
msgid "Paused"
msgstr "En pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Veuillez <0>configurer un serveur SMTP</0> pour garantir la livraison des alertes."
@@ -793,7 +774,7 @@ msgstr "Veuillez créer un compte administrateur"
msgid "Please enable pop-ups for this site"
msgstr "Veuillez activer les pop-ups pour ce site"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Veuillez vous reconnecter"
@@ -920,7 +901,6 @@ msgstr "Trier par"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Statut"
@@ -1075,7 +1055,6 @@ msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
msgid "Unit preferences"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Token universel"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Joignable"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgid "Updated in real time. Click on a system to view information."
msgstr "Mis à jour en temps réel. Cliquez sur un système pour voir les informations."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "Configuration YAML"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Vos paramètres utilisateur ont été mis à jour."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Croatian\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dan} other {# dani}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# sat} other {# sati}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# sat} other {# sati}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 sat"
@@ -131,7 +125,6 @@ msgstr "Upozorenja"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Svi Sistemi"
@@ -259,10 +252,6 @@ msgstr "Provjerite logove za više detalja."
msgid "Check your notification service"
msgstr "Provjerite Vaš servis notifikacija"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Pritisnite za kopiranje"
@@ -437,10 +426,6 @@ msgstr "Dokumentacija"
msgid "Down"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -507,7 +492,7 @@ msgstr "Izvoz trenutne sistemske konfiguracije."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Provjera autentičnosti nije uspjela"
@@ -768,10 +753,6 @@ msgstr "Pauza"
msgid "Paused"
msgstr "Pauzirano"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku upozorenja."
@@ -793,7 +774,7 @@ msgstr "Molimo kreirajte administratorski račun"
msgid "Please enable pop-ups for this site"
msgstr "Omogućite skočne prozore za ovu stranicu"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Molimo prijavite se ponovno"
@@ -920,7 +901,6 @@ msgstr "Sortiraj po"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1075,7 +1055,6 @@ msgstr "Pokreće se kada iskorištenost bilo kojeg diska premaši prag"
msgid "Unit preferences"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgid "Updated in real time. Click on a system to view information."
msgstr "Ažurirano odmah. Kliknite na sistem za više informacija."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML Konfiguracija"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Vaše korisničke postavke su ažurirane."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# nap} other {# nap}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# óra} other {# óra}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# óra} other {# óra}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 óra"
@@ -131,7 +125,6 @@ msgstr "Riasztások"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Minden rendszer"
@@ -259,10 +252,6 @@ msgstr "Ellenőrizd a naplót a további részletekért."
msgid "Check your notification service"
msgstr "Ellenőrizd az értesítési szolgáltatásodat"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Kattints a másoláshoz"
@@ -437,10 +426,6 @@ msgstr "Dokumentáció"
msgid "Down"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -507,7 +492,7 @@ msgstr "Exportálja a jelenlegi rendszerkonfigurációt."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Hitelesítés sikertelen"
@@ -768,10 +753,6 @@ msgstr "Szüneteltetés"
msgid "Paused"
msgstr "Szüneteltetve"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Kérjük, <0>konfigurálj egy SMTP szervert</0> az értesítések kézbesítésének biztosítása érdekében."
@@ -793,7 +774,7 @@ msgstr "Kérjük, hozzon létre egy admin fiókot"
msgid "Please enable pop-ups for this site"
msgstr "Kérjük, engedélyezze a felugró ablakokat ezen az oldalon"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Kérjük jelentkezz be újra"
@@ -920,7 +901,6 @@ msgstr "Rendezés"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Állapot"
@@ -1075,7 +1055,6 @@ msgstr "Bekapcsol, ha a lemez érzékelő túllép egy küszöbértéket"
msgid "Unit preferences"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgid "Updated in real time. Click on a system to view information."
msgstr "Valós időben frissítve. Kattintson egy rendszerre az információk megtekintéséhez."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML konfiguráció"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "A felhasználói beállítások frissítésre kerültek."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: is\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Icelandic\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dagur} other {# dagar}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# klukkustund} other {# klukkustundir}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# klukkustund} other {# klukkustundir}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 klukkustund"
@@ -131,7 +125,6 @@ msgstr "Tilkynningar"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Öll kerfi"
@@ -259,10 +252,6 @@ msgstr "Skoðaðu logga til að sjá meiri upplýsingar."
msgid "Check your notification service"
msgstr "Athugaðu tilkynningaþjónustuna þína"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Smelltu til að afrita"
@@ -437,10 +426,6 @@ msgstr "Skjal"
msgid "Down"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -507,7 +492,7 @@ msgstr ""
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Villa í auðkenningu"
@@ -768,10 +753,6 @@ msgstr "Pása"
msgid "Paused"
msgstr "Í bið"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr ""
@@ -793,7 +774,7 @@ msgstr "Vinsamlegast búðu til admin aðgang"
msgid "Please enable pop-ups for this site"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Vinsamlegast skráðu þið inn aftur"
@@ -920,7 +901,6 @@ msgstr "Raða eftir"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Staða"
@@ -1075,7 +1055,6 @@ msgstr "Virkjast þegar diska notkun fer yfir þröskuld"
msgid "Unit preferences"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgid "Updated in real time. Click on a system to view information."
msgstr "Uppfærist í rauntíma. Veldu kerfi til að skoða upplýsingar."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Notenda stillingar vistaðar."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# giorno} other {# giorni}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ora} other {# ore}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuto} other {# minuti}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} di {1} righe selezionate."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ora} other {# ore}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 ora"
@@ -131,7 +125,6 @@ msgstr "Avvisi"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Tutti i Sistemi"
@@ -202,12 +195,12 @@ msgstr "Binario"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bit (Kbps, Mbps, Gbps)"
msgstr ""
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Byte (KB/s, MB/s, GB/s)"
msgstr ""
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
@@ -224,11 +217,11 @@ msgstr "Attenzione - possibile perdita di dati"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr "Celsius (°C)"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
msgstr "Modifica le unità di visualizzazione per le metriche."
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
@@ -259,10 +252,6 @@ msgstr "Controlla i log per maggiori dettagli."
msgid "Check your notification service"
msgstr "Controlla il tuo servizio di notifica"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Clicca su un sistema per visualizzare più informazioni."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Clicca per copiare"
@@ -283,7 +272,7 @@ msgstr "Conferma password"
#: src/components/routes/home.tsx
msgid "Connection is down"
msgstr "La connessione è interrotta"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -357,7 +346,7 @@ msgstr "Crea account"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr "Creato"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Critical (%)"
@@ -401,7 +390,7 @@ msgstr "I/O Disco"
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Unità disco"
msgstr ""
#: src/components/charts/disk-chart.tsx
#: src/components/routes/system.tsx
@@ -437,13 +426,9 @@ msgstr "Documentazione"
msgid "Down"
msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Durata"
msgstr ""
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -505,9 +490,9 @@ msgstr "Esporta la configurazione attuale dei tuoi sistemi."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Autenticazione fallita"
@@ -532,7 +517,7 @@ msgstr "Filtra..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr "Impronta digitale"
msgstr ""
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -686,7 +671,7 @@ msgstr "Traffico di rete delle interfacce pubbliche"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr "Unità rete"
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
@@ -768,10 +753,6 @@ msgstr "Pausa"
msgid "Paused"
msgstr "In pausa"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "In pausa ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Si prega di <0>configurare un server SMTP</0> per garantire la consegna degli avvisi."
@@ -793,7 +774,7 @@ msgstr "Si prega di creare un account amministratore"
msgid "Please enable pop-ups for this site"
msgstr "Si prega di abilitare i pop-up per questo sito"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Si prega di accedere nuovamente"
@@ -841,7 +822,7 @@ msgstr "Reimposta Password"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Resolved"
msgstr "Risolto"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Resume"
@@ -853,7 +834,7 @@ msgstr "Ruota token"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
msgstr "Righe per pagina"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
@@ -918,9 +899,8 @@ msgstr "Ordina per"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
msgid "State"
msgstr "Stato"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Stato"
@@ -942,7 +922,7 @@ msgstr "Sistema"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr "Medie di carico del sistema nel tempo"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
@@ -968,7 +948,7 @@ msgstr "Temperatura"
#: src/components/routes/settings/general.tsx
msgid "Temperature unit"
msgstr "Unità temperatura"
msgstr ""
#: src/components/routes/system.tsx
msgid "Temperatures of system sensors"
@@ -992,7 +972,7 @@ msgstr "Questa azione non può essere annullata. Questo eliminerà permanentemen
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "This will permanently delete all selected records from the database."
msgstr "Questo eliminerà permanentemente tutti i record selezionati dal database."
msgstr ""
#: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}"
@@ -1036,15 +1016,15 @@ msgstr "I token e le impronte digitali vengono utilizzati per autenticare le con
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Si attiva quando la media di carico di 1 minuto supera una soglia"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "Si attiva quando la media di carico di 15 minuti supera una soglia"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Si attiva quando la media di carico di 5 minuti supera una soglia"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1073,9 +1053,8 @@ msgstr "Attiva quando l'utilizzo di un disco supera una soglia"
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
msgstr "Preferenze unità"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Token universale"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Attivo"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Attivo ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "Aggiornato in tempo reale. Clicca su un sistema per visualizzare le informazioni."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1122,7 +1101,7 @@ msgstr "Utenti"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr "Valore"
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "View"
@@ -1130,7 +1109,7 @@ msgstr "Vista"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Visualizza i tuoi 200 avvisi più recenti."
msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
@@ -1183,3 +1162,4 @@ msgstr "Configurazione YAML"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Le impostazioni utente sono state aggiornate."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:20\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# 日} other {# 日}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# 時間} other {# 時間}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# 分} few {# 分} many {# 分} other {# 分}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{1}行のうち{0}行が選択されました。"
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 時間} other {# 時間}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1時間"
@@ -131,7 +125,6 @@ msgstr "アラート"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "すべてのシステム"
@@ -259,10 +252,6 @@ msgstr "詳細についてはログを確認してください。"
msgid "Check your notification service"
msgstr "通知サービスを確認してください"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "システムをクリックして詳細を表示します。"
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "クリックしてコピー"
@@ -437,10 +426,6 @@ msgstr "ドキュメント"
msgid "Down"
msgstr "停止"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "停止 ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "期間"
@@ -507,7 +492,7 @@ msgstr "現在のシステム設定をエクスポートします。"
msgid "Fahrenheit (°F)"
msgstr "華氏 (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "認証に失敗しました"
@@ -768,10 +753,6 @@ msgstr "一時停止"
msgid "Paused"
msgstr "一時停止中"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "一時停止 ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "アラートが配信されるように<0>SMTPサーバーを設定</0>してください。"
@@ -793,7 +774,7 @@ msgstr "管理者アカウントを作成してください"
msgid "Please enable pop-ups for this site"
msgstr "このサイトのポップアップを有効にしてください"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "再度ログインしてください"
@@ -920,7 +901,6 @@ msgstr "並び替え基準"
msgid "State"
msgstr "状態"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "ステータス"
@@ -1075,7 +1055,6 @@ msgstr "ディスクの使用量がしきい値を超えたときにトリガー
msgid "Unit preferences"
msgstr "単位の設定"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "ユニバーサルトークン"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "正常"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "正常 ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "リアルタイムで更新されます。システムをクリックして情報を表示します。"
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML設定"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "ユーザー設定が更新されました。"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-31 15:44\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# 일} other {# 일}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# 시간} other {# 시간}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# 분} few {# 분} many {# 분} other {# 분}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{1}개의 행 중 {0}개가 선택되었습니다."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 시간} other {# 시간}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1시간"
@@ -131,7 +125,6 @@ msgstr "알림"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "모든 시스템"
@@ -259,10 +252,6 @@ msgstr "자세한 내용은 로그를 확인하세요."
msgid "Check your notification service"
msgstr "알림 서비스를 확인하세요."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "더 많은 정보를 보려면 시스템을 클릭하세요."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "클릭하여 복사"
@@ -437,10 +426,6 @@ msgstr "문서"
msgid "Down"
msgstr "오프라인"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "오프라인 ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "기간"
@@ -507,7 +492,7 @@ msgstr "현재 시스템 구성 내보내기"
msgid "Fahrenheit (°F)"
msgstr "화씨 (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "인증 실패"
@@ -768,10 +753,6 @@ msgstr "일시 중지"
msgid "Paused"
msgstr "일시 정지됨"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "일시 정지됨 ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "알림이 전달되도록 <0>SMTP 서버를 구성</0>하세요."
@@ -793,7 +774,7 @@ msgstr "관리자 계정을 생성하세요."
msgid "Please enable pop-ups for this site"
msgstr "이 사이트에 대해 팝업을 활성화하세요."
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "다시 로그인하세요."
@@ -920,7 +901,6 @@ msgstr "정렬 기준"
msgid "State"
msgstr "상태"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "상태"
@@ -1075,7 +1055,6 @@ msgstr "디스크 사용량이 임계값을 초과할 때 트리거됩니다."
msgid "Unit preferences"
msgstr "단위 기본 설정"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "범용 토큰"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "온라인"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "온라인 ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "실시간으로 업데이트됩니다. 시스템을 클릭하여 정보를 확인하세요."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML 구성"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "사용자 설정이 업데이트되었습니다."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dag} other {# dagen}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# uur} other {# uren}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuut} other {# minuten}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} van de {1} rij(en) geselecteerd."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# uur} other {# uren}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 uur"
@@ -131,7 +125,6 @@ msgstr "Waarschuwingen"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle systemen"
@@ -259,10 +252,6 @@ msgstr "Controleer de logs voor meer details."
msgid "Check your notification service"
msgstr "Controleer je meldingsservice"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Klik op een systeem om meer informatie te bekijken."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Klik om te kopiëren"
@@ -437,10 +426,6 @@ msgstr "Documentatie"
msgid "Down"
msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr "Duur"
@@ -507,7 +492,7 @@ msgstr "Exporteer je huidige systeemconfiguratie."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Authenticatie mislukt"
@@ -768,10 +753,6 @@ msgstr "Pauze"
msgid "Paused"
msgstr "Gepauzeerd"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Gepauzeerd ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "<0>Configureer een SMTP-server </0> om ervoor te zorgen dat waarschuwingen worden afgeleverd."
@@ -793,7 +774,7 @@ msgstr "Maak een beheerdersaccount aan"
msgid "Please enable pop-ups for this site"
msgstr "Activeer pop-ups voor deze website"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Meld je opnieuw aan"
@@ -920,7 +901,6 @@ msgstr "Sorteren op"
msgid "State"
msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1075,7 +1055,6 @@ msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrij
msgid "Unit preferences"
msgstr "Eenheid voorkeuren"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr "Universele token"
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Online"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Online ({upSystemsLength})"
msgid "Updated in real time. Click on a system to view information."
msgstr "In realtime bijgewerkt. Klik op een systeem om informatie te bekijken."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML Configuratie"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Je gebruikersinstellingen zijn bijgewerkt."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-08-25 21:21\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,22 +23,16 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dag} other {# dager}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# time} other {# timer}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# time} other {# timer}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 time"
@@ -131,7 +125,6 @@ msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle Systemer"
@@ -259,10 +252,6 @@ msgstr "Sjekk loggene for flere detaljer."
msgid "Check your notification service"
msgstr "Sjekk din meldingstjeneste"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
msgstr "Klikk for å kopiere"
@@ -437,10 +426,6 @@ msgstr "Dokumentasjon"
msgid "Down"
msgstr "Nede"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -507,7 +492,7 @@ msgstr "Eksporter din nåværende systemkonfigurasjon"
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Failed to authenticate"
msgstr "Autentisering mislyktes"
@@ -768,10 +753,6 @@ msgstr "Pause"
msgid "Paused"
msgstr "Satt på Pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Vennligst <0>konfigurer en SMTP-server</0> for å forsikre deg om at varsler blir levert."
@@ -793,7 +774,7 @@ msgstr "Vennligst opprett en admin-konto"
msgid "Please enable pop-ups for this site"
msgstr "Vennligst aktiver pop-ups for nettsiden"
#: src/lib/api.ts
#: src/lib/utils.ts
msgid "Please log in again"
msgstr "Vennligst logg inn på nytt"
@@ -920,7 +901,6 @@ msgstr "Sorter Etter"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1075,7 +1055,6 @@ msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grensever
msgid "Unit preferences"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
@@ -1092,8 +1071,8 @@ msgid "Up"
msgstr "Oppe"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgid "Updated in real time. Click on a system to view information."
msgstr "Oppdatert i sanntid. Klikk på et system for å se mer informasjon."
#: src/components/routes/system.tsx
msgid "Uptime"
@@ -1183,3 +1162,4 @@ msgstr "YAML Konfigurasjon"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Dine brukerinnstillinger har blitt oppdatert."

Some files were not shown because too many files have changed in this diff Show More