mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-28 08:26:18 +01:00
Compare commits
212 Commits
v0.11.0
...
03ed830a02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ed830a02 | ||
|
|
025a80dedf | ||
|
|
e68a74429e | ||
|
|
8eac0f1deb | ||
|
|
9e3a429fa9 | ||
|
|
bb35175808 | ||
|
|
9d95dd818a | ||
|
|
afd7385a4b | ||
|
|
c0737f8696 | ||
|
|
039639d5db | ||
|
|
ab7a18a79c | ||
|
|
6726702a9f | ||
|
|
7cdee9cb5e | ||
|
|
c583214578 | ||
|
|
9fb023d54b | ||
|
|
492485b347 | ||
|
|
c9d4115268 | ||
|
|
7c814f24cf | ||
|
|
30d89ad537 | ||
|
|
5e02b76f93 | ||
|
|
f82c4d7bdf | ||
|
|
04f4221ab1 | ||
|
|
6c231eb02f | ||
|
|
017365bb55 | ||
|
|
824149927e | ||
|
|
4c8f2452a8 | ||
|
|
196cbd7f78 | ||
|
|
3c9c5e7938 | ||
|
|
67a35ec7ea | ||
|
|
3a3516d05e | ||
|
|
cf22a44e03 | ||
|
|
fe3c40d528 | ||
|
|
efa74665db | ||
|
|
49d94321fd | ||
|
|
250aa61322 | ||
|
|
39cf17d24b | ||
|
|
a2ea3d104e | ||
|
|
ab0f5c765f | ||
|
|
c6a4903b51 | ||
|
|
2e14315e3b | ||
|
|
da3543017d | ||
|
|
e2b9144cef | ||
|
|
d897325933 | ||
|
|
3382ff89e7 | ||
|
|
d07dd668e6 | ||
|
|
652c6f3303 | ||
|
|
05134cb354 | ||
|
|
397fdfd68b | ||
|
|
278e066c6f | ||
|
|
6573a599f6 | ||
|
|
059575dc83 | ||
|
|
40f55f6959 | ||
|
|
e586aeb17a | ||
|
|
dc3f2fdb51 | ||
|
|
a8bcb2022b | ||
|
|
9db2daa640 | ||
|
|
0364c59ae2 | ||
|
|
741c83d476 | ||
|
|
f061352ddc | ||
|
|
fa77ddfffe | ||
|
|
27f4a648d1 | ||
|
|
d0ff1ccc16 | ||
|
|
8edb928e50 | ||
|
|
97e31dd5a9 | ||
|
|
181bdab303 | ||
|
|
30756bb079 | ||
|
|
a0540821db | ||
|
|
1779a056c3 | ||
|
|
258c70f2d8 | ||
|
|
ea50c711b2 | ||
|
|
b30d957b38 | ||
|
|
e6abfc6a5d | ||
|
|
7d356d6edb | ||
|
|
30766df9fa | ||
|
|
eaf992f3f1 | ||
|
|
27b81aa489 | ||
|
|
5a903f472e | ||
|
|
4985474b0f | ||
|
|
0632db3c19 | ||
|
|
6980a4ef86 | ||
|
|
ee22bf8bcb | ||
|
|
ae4c402b99 | ||
|
|
7281094dc3 | ||
|
|
699e81eb1c | ||
|
|
a6065b6a19 | ||
|
|
b278c92e54 | ||
|
|
83d41ed1f9 | ||
|
|
7fa39a120f | ||
|
|
4b59748c0c | ||
|
|
ab2ef5bcfc | ||
|
|
2524b0d359 | ||
|
|
f84b159309 | ||
|
|
d5e5f8678a | ||
|
|
c91e0a9faa | ||
|
|
c6a6a353a5 | ||
|
|
7d1f8f5cae | ||
|
|
64a5ef4dfe | ||
|
|
030d2b6684 | ||
|
|
938e87db8b | ||
|
|
31ed898a58 | ||
|
|
1b8ad81046 | ||
|
|
ff3874491c | ||
|
|
fe81553c6a | ||
|
|
6e8b3f6265 | ||
|
|
9c87911a7f | ||
|
|
082cbb36e7 | ||
|
|
8347cf0e5c | ||
|
|
abfafae0d9 | ||
|
|
6496f35915 | ||
|
|
7fee3da2e8 | ||
|
|
443e203d9c | ||
|
|
24875b5733 | ||
|
|
66eaf45c79 | ||
|
|
2b9fb8b1ec | ||
|
|
06e0ae4733 | ||
|
|
4cc0bfeb0f | ||
|
|
320d0e5d97 | ||
|
|
67953bb6af | ||
|
|
123c57ded9 | ||
|
|
a7991ca184 | ||
|
|
1b01410533 | ||
|
|
1687919f31 | ||
|
|
6046dbb727 | ||
|
|
2505b16faa | ||
|
|
c4352e65fb | ||
|
|
7b2e9ccdcb | ||
|
|
75a87cddbf | ||
|
|
bda7b9b48d | ||
|
|
65d6ec1918 | ||
|
|
0e16fca07f | ||
|
|
b7e574f379 | ||
|
|
113f1833e1 | ||
|
|
a0d3e60d29 | ||
|
|
df7f12bfec | ||
|
|
02e88d99e5 | ||
|
|
a11ceb36d1 | ||
|
|
d3a373338d | ||
|
|
e1ef05da8c | ||
|
|
c01bfd9332 | ||
|
|
209e82aed2 | ||
|
|
c66e740d52 | ||
|
|
551ae49c2e | ||
|
|
906daa6f2a | ||
|
|
49e55943c1 | ||
|
|
9e75cbc1ef | ||
|
|
79190a2c51 | ||
|
|
32960c7c35 | ||
|
|
cdff974a8a | ||
|
|
20a6ef129c | ||
|
|
d1e5310f83 | ||
|
|
152173eab5 | ||
|
|
c2aec69638 | ||
|
|
16c97fe77b | ||
|
|
bed5bc2791 | ||
|
|
424c7aceff | ||
|
|
8eb101e714 | ||
|
|
f2e69e2a80 | ||
|
|
41b4dbce98 | ||
|
|
c3432fc7b5 | ||
|
|
b167244e28 | ||
|
|
bcca6a5a9d | ||
|
|
6b661b4878 | ||
|
|
bc537edb73 | ||
|
|
561a3e8aaf | ||
|
|
f173ea37da | ||
|
|
0d9f2ba06a | ||
|
|
4e772e6008 | ||
|
|
b959d97502 | ||
|
|
735cbe2cf0 | ||
|
|
0f5a470495 | ||
|
|
a42e99370e | ||
|
|
abfae78af1 | ||
|
|
e36c40c4a9 | ||
|
|
bf7b2ae598 | ||
|
|
f71ee4b058 | ||
|
|
f2466eb37d | ||
|
|
7244c7130b | ||
|
|
f2e84a9d3e | ||
|
|
04a1ee5e4e | ||
|
|
9b83088897 | ||
|
|
13b30aa255 | ||
|
|
2bd25e9e8d | ||
|
|
4fe192bf28 | ||
|
|
2056ae285f | ||
|
|
a02e7a0a69 | ||
|
|
b1a9e90034 | ||
|
|
20916fab3e | ||
|
|
0c777cca72 | ||
|
|
44e30ad429 | ||
|
|
9e30786dda | ||
|
|
1f677773e7 | ||
|
|
882289da91 | ||
|
|
fbbc4eff27 | ||
|
|
b78231f677 | ||
|
|
332e2d14a9 | ||
|
|
e088b88c84 | ||
|
|
09840f95d9 | ||
|
|
de03bb658a | ||
|
|
bcadfeb729 | ||
|
|
ec582a9171 | ||
|
|
0c3eaefc90 | ||
|
|
d6b5866f90 | ||
|
|
2ba629e4b4 | ||
|
|
6e9341b7ff | ||
|
|
89499a341b | ||
|
|
9c88845798 | ||
|
|
c9f65f63e6 | ||
|
|
996620c8e0 | ||
|
|
626c1358d9 | ||
|
|
19dd39b7db | ||
|
|
d482b50e31 | ||
|
|
f61ec49c76 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -3,7 +3,7 @@ name: Make release and binaries
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,5 +15,4 @@ beszel/build
|
||||
*timestamp*
|
||||
.swc
|
||||
beszel/site/src/locales/**/*.ts
|
||||
*.bak
|
||||
__debug_*
|
||||
*.bak
|
||||
@@ -29,7 +29,6 @@ builds:
|
||||
- linux
|
||||
- darwin
|
||||
- freebsd
|
||||
- openbsd
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
@@ -40,8 +39,6 @@ builds:
|
||||
ignore:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
@@ -50,7 +47,7 @@ builds:
|
||||
goarch: riscv64
|
||||
|
||||
archives:
|
||||
- id: beszel-agent
|
||||
- id: beszel
|
||||
format: tar.gz
|
||||
builds:
|
||||
- beszel-agent
|
||||
@@ -62,7 +59,7 @@ archives:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
- id: beszel
|
||||
- id: beszel-agent
|
||||
format: tar.gz
|
||||
builds:
|
||||
- beszel
|
||||
@@ -114,65 +111,6 @@ nfpms:
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
#config: ../supplemental/debian/config.sh
|
||||
|
||||
scoops:
|
||||
- ids: [beszel-agent]
|
||||
name: beszel-agent
|
||||
repository:
|
||||
owner: henrygd
|
||||
name: beszel-scoops
|
||||
homepage: 'https://beszel.dev'
|
||||
description: 'Agent for Beszel, a lightweight server monitoring platform.'
|
||||
license: MIT
|
||||
|
||||
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
|
||||
# chocolateys:
|
||||
# - title: Beszel Agent
|
||||
# ids: [beszel-agent]
|
||||
# package_source_url: https://github.com/henrygd/beszel-chocolatey
|
||||
# owners: henrygd
|
||||
# authors: henrygd
|
||||
# summary: 'Agent for Beszel, a lightweight server monitoring platform.'
|
||||
# description: |
|
||||
# Beszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions.
|
||||
|
||||
# It has a friendly web interface, simple configuration, and is ready to use out of the box. It supports automatic backup, multi-user, OAuth authentication, and API access.
|
||||
# license_url: https://github.com/henrygd/beszel/blob/main/LICENSE
|
||||
# project_url: https://beszel.dev
|
||||
# project_source_url: https://github.com/henrygd/beszel
|
||||
# docs_url: https://beszel.dev/guide/getting-started
|
||||
# icon_url: https://cdn.jsdelivr.net/gh/selfhst/icons/png/beszel.png
|
||||
# bug_tracker_url: https://github.com/henrygd/beszel/issues
|
||||
# copyright: 2025 henrygd
|
||||
# tags: foss cross-platform admin monitoring
|
||||
# require_license_acceptance: false
|
||||
# release_notes: 'https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}'
|
||||
|
||||
brews:
|
||||
- ids: [beszel-agent]
|
||||
name: beszel-agent
|
||||
repository:
|
||||
owner: henrygd
|
||||
name: homebrew-beszel
|
||||
homepage: 'https://beszel.dev'
|
||||
description: 'Agent for Beszel, a lightweight server monitoring platform.'
|
||||
license: MIT
|
||||
extra_install: |
|
||||
(bin/"beszel-agent-launcher").write <<~EOS
|
||||
#!/bin/bash
|
||||
set -a
|
||||
if [ -f "$HOME/.config/beszel/beszel-agent.env" ]; then
|
||||
source "$HOME/.config/beszel/beszel-agent.env"
|
||||
fi
|
||||
set +a
|
||||
exec #{bin}/beszel-agent "$@"
|
||||
EOS
|
||||
(bin/"beszel-agent-launcher").chmod 0755
|
||||
service: |
|
||||
run ["#{bin}/beszel-agent-launcher"]
|
||||
log_path "#{Dir.home}/.cache/beszel/beszel-agent.log"
|
||||
error_log_path "#{Dir.home}/.cache/beszel/beszel-agent.log"
|
||||
keep_alive true
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
|
||||
@@ -14,10 +14,6 @@ clean:
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
test: export GOEXPERIMENT=synctest
|
||||
test:
|
||||
go test -tags=testing ./...
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
|
||||
@@ -7,63 +7,50 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// cli options
|
||||
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.
|
||||
key string // key is the public key(s) for SSH authentication.
|
||||
addr string // addr is the address or port to listen on.
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// parseFlags parses the command line flags and populates the config struct.
|
||||
func (opts *cmdOptions) parseFlags() {
|
||||
flag.StringVar(&opts.key, "key", "", "Public key(s) for SSH authentication")
|
||||
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
|
||||
flag.StringVar(&opts.addr, "addr", "", "Address or port to listen on")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("Usage: %s [command] [flags]\n", os.Args[0])
|
||||
fmt.Println("\nCommands:")
|
||||
fmt.Println(" health Check if the agent is running")
|
||||
fmt.Println(" help Display this help message")
|
||||
fmt.Println(" update Update to the latest version")
|
||||
fmt.Println(" version Display the version")
|
||||
fmt.Println("\nFlags:")
|
||||
fmt.Printf("Usage: %s [options] [subcommand]\n", os.Args[0])
|
||||
fmt.Println("\nOptions:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("\nSubcommands:")
|
||||
fmt.Println(" version Display the version")
|
||||
fmt.Println(" help Display this help message")
|
||||
fmt.Println(" update Update the agent to the latest version")
|
||||
}
|
||||
}
|
||||
|
||||
subcommand := ""
|
||||
if len(os.Args) > 1 {
|
||||
subcommand = os.Args[1]
|
||||
// handleSubcommand handles subcommands such as version, help, and update.
|
||||
// It returns true if a subcommand was handled, false otherwise.
|
||||
func handleSubcommand() bool {
|
||||
if len(os.Args) <= 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch subcommand {
|
||||
case "-v", "version":
|
||||
switch os.Args[1] {
|
||||
case "version", "-v":
|
||||
fmt.Println(beszel.AppName+"-agent", beszel.Version)
|
||||
return true
|
||||
os.Exit(0)
|
||||
case "help":
|
||||
flag.Usage()
|
||||
return true
|
||||
os.Exit(0)
|
||||
case "update":
|
||||
agent.Update()
|
||||
return true
|
||||
case "health":
|
||||
// for health, we need to parse flags first to get the listen address
|
||||
args := append(os.Args[2:], subcommand)
|
||||
flag.CommandLine.Parse(args)
|
||||
addr := opts.getAddress()
|
||||
network := agent.GetNetwork(addr)
|
||||
err := agent.Health(addr, network)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Print("ok")
|
||||
return true
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -92,18 +79,46 @@ func (opts *cmdOptions) loadPublicKeys() ([]ssh.PublicKey, error) {
|
||||
return agent.ParseKeys(string(pubKey))
|
||||
}
|
||||
|
||||
// getAddress gets the address to listen on from the command line flag, environment variable, or default value.
|
||||
func (opts *cmdOptions) getAddress() string {
|
||||
return agent.GetAddress(opts.listen)
|
||||
// Try command line flag first
|
||||
if opts.addr != "" {
|
||||
return opts.addr
|
||||
}
|
||||
// Try environment variables
|
||||
if addr, ok := agent.GetEnv("ADDR"); ok && addr != "" {
|
||||
return addr
|
||||
}
|
||||
// Legacy PORT environment variable support
|
||||
if port, ok := agent.GetEnv("PORT"); ok && port != "" {
|
||||
return port
|
||||
}
|
||||
return ":45876"
|
||||
}
|
||||
|
||||
// getNetwork returns the network type to use for the server.
|
||||
func (opts *cmdOptions) getNetwork() string {
|
||||
if network, _ := agent.GetEnv("NETWORK"); network != "" {
|
||||
return network
|
||||
}
|
||||
if strings.HasPrefix(opts.addr, "/") {
|
||||
return "unix"
|
||||
}
|
||||
return "tcp"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var opts cmdOptions
|
||||
subcommandHandled := opts.parse()
|
||||
opts.parseFlags()
|
||||
|
||||
if subcommandHandled {
|
||||
if handleSubcommand() {
|
||||
return
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
opts.addr = opts.getAddress()
|
||||
|
||||
var serverConfig agent.ServerOptions
|
||||
var err error
|
||||
serverConfig.Keys, err = opts.loadPublicKeys()
|
||||
@@ -111,9 +126,8 @@ func main() {
|
||||
log.Fatal("Failed to load public keys:", err)
|
||||
}
|
||||
|
||||
addr := opts.getAddress()
|
||||
serverConfig.Addr = addr
|
||||
serverConfig.Network = agent.GetNetwork(addr)
|
||||
serverConfig.Addr = opts.addr
|
||||
serverConfig.Network = opts.getNetwork()
|
||||
|
||||
agent := agent.NewAgent()
|
||||
if err := agent.StartServer(serverConfig); err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel/internal/agent"
|
||||
"crypto/ed25519"
|
||||
"flag"
|
||||
"os"
|
||||
@@ -28,22 +27,22 @@ func TestGetAddress(t *testing.T) {
|
||||
{
|
||||
name: "use address from flag",
|
||||
opts: cmdOptions{
|
||||
listen: "8080",
|
||||
addr: "8080",
|
||||
},
|
||||
expected: ":8080",
|
||||
expected: "8080",
|
||||
},
|
||||
{
|
||||
name: "use unix socket from flag",
|
||||
opts: cmdOptions{
|
||||
listen: "/tmp/beszel.sock",
|
||||
addr: "/tmp/beszel.sock",
|
||||
},
|
||||
expected: "/tmp/beszel.sock",
|
||||
},
|
||||
{
|
||||
name: "use LISTEN env var",
|
||||
name: "use ADDR env var",
|
||||
opts: cmdOptions{},
|
||||
envVars: map[string]string{
|
||||
"LISTEN": "1.2.3.4:9090",
|
||||
"ADDR": "1.2.3.4:9090",
|
||||
},
|
||||
expected: "1.2.3.4:9090",
|
||||
},
|
||||
@@ -53,26 +52,26 @@ func TestGetAddress(t *testing.T) {
|
||||
envVars: map[string]string{
|
||||
"PORT": "7070",
|
||||
},
|
||||
expected: ":7070",
|
||||
expected: "7070",
|
||||
},
|
||||
{
|
||||
name: "use unix socket from env var",
|
||||
opts: cmdOptions{
|
||||
listen: "",
|
||||
addr: "",
|
||||
},
|
||||
envVars: map[string]string{
|
||||
"LISTEN": "/tmp/beszel.sock",
|
||||
"ADDR": "/tmp/beszel.sock",
|
||||
},
|
||||
expected: "/tmp/beszel.sock",
|
||||
},
|
||||
{
|
||||
name: "flag takes precedence over env vars",
|
||||
opts: cmdOptions{
|
||||
listen: ":8080",
|
||||
addr: ":8080",
|
||||
},
|
||||
envVars: map[string]string{
|
||||
"LISTEN": ":9090",
|
||||
"PORT": "7070",
|
||||
"ADDR": ":9090",
|
||||
"PORT": "7070",
|
||||
},
|
||||
expected: ":8080",
|
||||
},
|
||||
@@ -202,27 +201,27 @@ func TestGetNetwork(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "only port",
|
||||
opts: cmdOptions{listen: "8080"},
|
||||
opts: cmdOptions{addr: "8080"},
|
||||
expected: "tcp",
|
||||
},
|
||||
{
|
||||
name: "ipv4 address",
|
||||
opts: cmdOptions{listen: "1.2.3.4:8080"},
|
||||
opts: cmdOptions{addr: "1.2.3.4:8080"},
|
||||
expected: "tcp",
|
||||
},
|
||||
{
|
||||
name: "ipv6 address",
|
||||
opts: cmdOptions{listen: "[2001:db8::1]:8080"},
|
||||
opts: cmdOptions{addr: "[2001:db8::1]:8080"},
|
||||
expected: "tcp",
|
||||
},
|
||||
{
|
||||
name: "unix network",
|
||||
opts: cmdOptions{listen: "/tmp/beszel.sock"},
|
||||
opts: cmdOptions{addr: "/tmp/beszel.sock"},
|
||||
expected: "unix",
|
||||
},
|
||||
{
|
||||
name: "env var network",
|
||||
opts: cmdOptions{listen: ":8080"},
|
||||
opts: cmdOptions{addr: ":8080"},
|
||||
envVars: map[string]string{"NETWORK": "tcp4"},
|
||||
expected: "tcp4",
|
||||
},
|
||||
@@ -234,7 +233,7 @@ func TestGetNetwork(t *testing.T) {
|
||||
for k, v := range tt.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
network := agent.GetNetwork(tt.opts.listen)
|
||||
network := tt.opts.getNetwork()
|
||||
assert.Equal(t, tt.expected, network)
|
||||
})
|
||||
}
|
||||
@@ -257,32 +256,32 @@ func TestParseFlags(t *testing.T) {
|
||||
name: "no flags",
|
||||
args: []string{"cmd"},
|
||||
expected: cmdOptions{
|
||||
key: "",
|
||||
listen: "",
|
||||
key: "",
|
||||
addr: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key flag only",
|
||||
args: []string{"cmd", "-key", "testkey"},
|
||||
expected: cmdOptions{
|
||||
key: "testkey",
|
||||
listen: "",
|
||||
key: "testkey",
|
||||
addr: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addr flag only",
|
||||
args: []string{"cmd", "-listen", ":8080"},
|
||||
args: []string{"cmd", "-addr", ":8080"},
|
||||
expected: cmdOptions{
|
||||
key: "",
|
||||
listen: ":8080",
|
||||
key: "",
|
||||
addr: ":8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both flags",
|
||||
args: []string{"cmd", "-key", "testkey", "-listen", ":8080"},
|
||||
args: []string{"cmd", "-key", "testkey", "-addr", ":8080"},
|
||||
expected: cmdOptions{
|
||||
key: "testkey",
|
||||
listen: ":8080",
|
||||
key: "testkey",
|
||||
addr: ":8080",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -294,7 +293,7 @@ func TestParseFlags(t *testing.T) {
|
||||
os.Args = tt.args
|
||||
|
||||
var opts cmdOptions
|
||||
opts.parse()
|
||||
opts.parseFlags()
|
||||
flag.Parse()
|
||||
|
||||
assert.Equal(t, tt.expected, opts)
|
||||
|
||||
@@ -1,99 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/hub"
|
||||
_ "beszel/migrations"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// handle health check first to prevent unneeded execution
|
||||
if len(os.Args) > 3 && os.Args[1] == "health" {
|
||||
url := os.Args[3]
|
||||
if err := checkHealth(url); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Print("ok")
|
||||
return
|
||||
}
|
||||
|
||||
baseApp := getBaseApp()
|
||||
h := hub.NewHub(baseApp)
|
||||
if err := h.StartHub(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// getBaseApp creates a new PocketBase app with the default config
|
||||
func getBaseApp() *pocketbase.PocketBase {
|
||||
isDev := os.Getenv("ENV") == "dev"
|
||||
|
||||
baseApp := pocketbase.NewWithConfig(pocketbase.Config{
|
||||
DefaultDataDir: beszel.AppName + "_data",
|
||||
DefaultDev: isDev,
|
||||
})
|
||||
baseApp.RootCmd.Version = beszel.Version
|
||||
baseApp.RootCmd.Use = beszel.AppName
|
||||
baseApp.RootCmd.Short = ""
|
||||
// add update command
|
||||
baseApp.RootCmd.AddCommand(&cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update " + beszel.AppName + " to the latest version",
|
||||
Run: hub.Update,
|
||||
})
|
||||
// add health command
|
||||
baseApp.RootCmd.AddCommand(newHealthCmd())
|
||||
|
||||
// enable auto creation of migration files when making collection changes in the Admin UI
|
||||
migratecmd.MustRegister(baseApp, baseApp.RootCmd, migratecmd.Config{
|
||||
Automigrate: isDev,
|
||||
Dir: "../../migrations",
|
||||
})
|
||||
|
||||
return baseApp
|
||||
}
|
||||
|
||||
func newHealthCmd() *cobra.Command {
|
||||
var baseURL string
|
||||
|
||||
healthCmd := &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Check health of running hub",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := checkHealth(baseURL); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
},
|
||||
}
|
||||
healthCmd.Flags().StringVar(&baseURL, "url", "", "base URL")
|
||||
healthCmd.MarkFlagRequired("url")
|
||||
return healthCmd
|
||||
}
|
||||
|
||||
// checkHealth checks the health of the hub.
|
||||
func checkHealth(baseURL string) error {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 3,
|
||||
}
|
||||
healthURL := baseURL + "/api/health"
|
||||
resp, err := client.Get(healthURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("%s returned status %d", healthURL, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
hub.NewHub().Run()
|
||||
}
|
||||
|
||||
@@ -1,48 +1,65 @@
|
||||
module beszel
|
||||
|
||||
go 1.24.2
|
||||
|
||||
// lock shoutrrr to specific version to allow review before updating
|
||||
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/nicholas-fedor/shoutrrr v0.8.8
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.27.1
|
||||
github.com/pocketbase/pocketbase v0.25.0
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||
github.com/shirou/gopsutil/v4 v4.25.3
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/spf13/cast v1.7.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.59 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.59 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
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.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
@@ -51,18 +68,26 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.6 // 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/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/image v0.26.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
modernc.org/libc v1.64.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.40.0 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.220.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.10.0 // indirect
|
||||
modernc.org/sqlite v1.37.0 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
modernc.org/sqlite v1.34.5 // indirect
|
||||
)
|
||||
|
||||
338
beszel/go.sum
338
beszel/go.sum
@@ -1,14 +1,75 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
|
||||
cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
|
||||
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
|
||||
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.6 h1:fqgqEKK5HaZVWLQoLiC9Q+xDlSp+1LYidp6ybGE2OGg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.6/go.mod h1:Ft+WLODzDQmCTHDvqAH1JfC2xxbZ0MxpZAcJqmE1LTQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.59 h1:9btwmrt//Q6JcSdgJOLI98sdr5p7tssS9yAsGe8aKP4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.59/go.mod h1:NM8fM6ovI3zak23UISdWidyZuI1ghNe2xjzUZAyT+08=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 h1:KwsodFKVQTlI5EyhRSugALzsV6mG/SGrdjlMXSZSdso=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28/go.mod h1:EY3APf9MzygVhKuPXAc5H+MkGb8k/DOSQjWS0LgkKqI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.59 h1:5Vsrfdlf9KQP3leGX1dD7VwZq/3HAerEFoXAII4t6zo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.59/go.mod h1:7XTNs3NYApJjkx6A2Fk9qq23qBuBnIU58k3fKC2Fr1I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 h1:OIHj/nAhVzIXGzbAE+4XmZ8FPvro3THr6NlqErJc3wY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32/go.mod h1:LiBEsDo34OJXqdDlRGsilhlIiXR7DL+6Cx2f4p1EgzI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 h1:cCBJaT7EeEojpJ4s7wTDbhZlHVJOgNHN7iw6qVurGaw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6/go.mod h1:WYH1ABybY7JK9TITPnk6ZlP7gQB8psI4c9qDmMsnLSA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 h1:OBsrtam3rk8NfBEq7OLOMm5HtQ9Yyw32X4UQMya/wjw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13/go.mod h1:3U4gFA5pmoCOja7aq4nSaIAGbaOHv2Yl2ug018cmC+Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.4 h1:DJYjOvNgC30JAcDCRmtQHoYK4trc7XetDXRTEAReGKA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.4/go.mod h1:KuLNrwYJFaC2AVZ+CVVc12k9NyqwgWsoNNHjwqF6QNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 h1:/eE3DogBjYlvlbhd2ssWyeuovWunHLxfgw3s/OJa4GQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15/go.mod h1:2PCJYpi7EKeA5SkStAmZlF6fi0uUABuhtF8ILHjGc3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 h1:M/zwXiL2iXUrHputuXgmO94TVNmcenPHxgLXLutodKE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14/go.mod h1:RVwIw3y/IqxC2YEXSIkAzRDdEU1iRabDPaYjpGCbCGQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 h1:TzeR06UCMUq+KA3bDkujxK1GVGy+G8qQN/QVYzGLkQE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14/go.mod h1:dspXf/oYWGWo6DEvj98wpaTeqt5+DMidZD0A9BYTizc=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
@@ -19,55 +80,94 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
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/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
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=
|
||||
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
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=
|
||||
@@ -75,32 +175,31 @@ 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-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
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/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
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/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.27.1 h1:KGCsS8idUVTC5QHxTj91qHDhIXOb5Yb50wwHhNvJRTQ=
|
||||
github.com/pocketbase/pocketbase v0.27.1/go.mod h1:aTpwwloVJzeJ7MlwTRrbI/x62QNR2/kkCrovmyrXpqs=
|
||||
github.com/pocketbase/pocketbase v0.25.0 h1:/4YQq1hd0muvhzbERyUTVNh88N0BCj5diqK0jtLN6k8=
|
||||
github.com/pocketbase/pocketbase v0.25.0/go.mod h1:tOtOv7f3vJhAiyUluIwV9JPuKeknZRQ9F6uJE3W/ntI=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
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=
|
||||
@@ -108,82 +207,156 @@ github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzx
|
||||
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.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng=
|
||||
gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20190213061140-3a22650c66bd/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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
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.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20200930185726-fdedc70b468f/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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns=
|
||||
google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY=
|
||||
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.4.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=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM=
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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=
|
||||
@@ -191,29 +364,32 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
||||
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
||||
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
|
||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||
modernc.org/fileutil v1.3.1/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/libc v1.64.0 h1:U0k8BD2d3cD3e9I8RLcZgJBHAcsJzbXx5mKGSb5pyJA=
|
||||
modernc.org/libc v1.64.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
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.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -4,36 +4,37 @@ package agent
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
sync.Mutex // Used to lock agent while collecting data
|
||||
debug bool // true if LOG_LEVEL is set to debug
|
||||
zfs bool // true if system has arcstats
|
||||
memCalc string // Memory calculation formula
|
||||
fsNames []string // List of filesystem device names being monitored
|
||||
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
|
||||
netInterfaces map[string]struct{} // Stores all valid network interfaces
|
||||
netIoStats system.NetIoStats // Keeps track of bandwidth usage
|
||||
dockerManager *dockerManager // Manages Docker API requests
|
||||
sensorConfig *SensorConfig // Sensors config
|
||||
systemInfo system.Info // Host system info
|
||||
gpuManager *GPUManager // Manages GPU data
|
||||
cache *SessionCache // Cache for system stats based on primary session ID
|
||||
sync.Mutex // Used to lock agent while collecting data
|
||||
debug bool // true if LOG_LEVEL is set to debug
|
||||
zfs bool // true if system has arcstats
|
||||
memCalc string // Memory calculation formula
|
||||
fsNames []string // List of filesystem device names being monitored
|
||||
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
|
||||
netInterfaces map[string]struct{} // Stores all valid network interfaces
|
||||
netIoStats system.NetIoStats // Keeps track of bandwidth usage
|
||||
dockerManager *dockerManager // Manages Docker API requests
|
||||
sensorsContext context.Context // Sensors context to override sys location
|
||||
sensorsWhitelist map[string]struct{} // List of sensors to monitor
|
||||
systemInfo system.Info // Host system info
|
||||
gpuManager *GPUManager // Manages GPU data
|
||||
}
|
||||
|
||||
func NewAgent() *Agent {
|
||||
agent := &Agent{
|
||||
fsStats: make(map[string]*system.FsStats),
|
||||
cache: NewSessionCache(69 * time.Second),
|
||||
}
|
||||
agent.memCalc, _ = GetEnv("MEM_CALC")
|
||||
agent.sensorConfig = agent.newSensorConfig()
|
||||
|
||||
// Set up slog with a log level determined by the LOG_LEVEL env var
|
||||
if logLevelStr, exists := GetEnv("LOG_LEVEL"); exists {
|
||||
switch strings.ToLower(logLevelStr) {
|
||||
@@ -49,6 +50,26 @@ func NewAgent() *Agent {
|
||||
|
||||
slog.Debug(beszel.Version)
|
||||
|
||||
// Set sensors context (allows overriding sys location for sensors)
|
||||
if sysSensors, exists := GetEnv("SYS_SENSORS"); exists {
|
||||
slog.Info("SYS_SENSORS", "path", sysSensors)
|
||||
agent.sensorsContext = context.WithValue(agent.sensorsContext,
|
||||
common.EnvKey, common.EnvMap{common.HostSysEnvKey: sysSensors},
|
||||
)
|
||||
} else {
|
||||
agent.sensorsContext = context.Background()
|
||||
}
|
||||
|
||||
// Set sensors whitelist
|
||||
if sensors, exists := GetEnv("SENSORS"); exists {
|
||||
agent.sensorsWhitelist = make(map[string]struct{})
|
||||
for _, sensor := range strings.Split(sensors, ",") {
|
||||
if sensor != "" {
|
||||
agent.sensorsWhitelist[sensor] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize system info / docker manager
|
||||
agent.initializeSystemInfo()
|
||||
agent.initializeDiskInfo()
|
||||
@@ -64,7 +85,7 @@ func NewAgent() *Agent {
|
||||
|
||||
// if debugging, print stats
|
||||
if agent.debug {
|
||||
slog.Debug("Stats", "data", agent.gatherStats(""))
|
||||
slog.Debug("Stats", "data", agent.gatherStats())
|
||||
}
|
||||
|
||||
return agent
|
||||
@@ -79,37 +100,29 @@ func GetEnv(key string) (value string, exists bool) {
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
func (a *Agent) gatherStats(sessionID string) *system.CombinedData {
|
||||
func (a *Agent) gatherStats() system.CombinedData {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
cachedData, ok := a.cache.Get(sessionID)
|
||||
if ok {
|
||||
slog.Debug("Cached stats", "session", sessionID)
|
||||
return cachedData
|
||||
}
|
||||
|
||||
*cachedData = system.CombinedData{
|
||||
slog.Debug("Getting stats")
|
||||
systemData := system.CombinedData{
|
||||
Stats: a.getSystemStats(),
|
||||
Info: a.systemInfo,
|
||||
}
|
||||
slog.Debug("System stats", "data", cachedData)
|
||||
|
||||
slog.Debug("System stats", "data", systemData)
|
||||
// add docker stats
|
||||
if containerStats, err := a.dockerManager.getDockerStats(); err == nil {
|
||||
cachedData.Containers = containerStats
|
||||
slog.Debug("Docker stats", "data", cachedData.Containers)
|
||||
systemData.Containers = containerStats
|
||||
slog.Debug("Docker stats", "data", systemData.Containers)
|
||||
} else {
|
||||
slog.Debug("Docker stats", "err", err)
|
||||
slog.Debug("Error getting docker stats", "err", err)
|
||||
}
|
||||
|
||||
cachedData.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||
// add extra filesystems
|
||||
systemData.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||
for name, stats := range a.fsStats {
|
||||
if !stats.Root && stats.DiskTotal > 0 {
|
||||
cachedData.Stats.ExtraFs[name] = stats
|
||||
systemData.Stats.ExtraFs[name] = stats
|
||||
}
|
||||
}
|
||||
slog.Debug("Extra filesystems", "data", cachedData.Stats.ExtraFs)
|
||||
|
||||
a.cache.Set(sessionID, cachedData)
|
||||
return cachedData
|
||||
slog.Debug("Extra filesystems", "data", systemData.Stats.ExtraFs)
|
||||
return systemData
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Not thread safe since we only access from gatherStats which is already locked
|
||||
type SessionCache struct {
|
||||
data *system.CombinedData
|
||||
lastUpdate time.Time
|
||||
primarySession string
|
||||
leaseTime time.Duration
|
||||
}
|
||||
|
||||
func NewSessionCache(leaseTime time.Duration) *SessionCache {
|
||||
return &SessionCache{
|
||||
leaseTime: leaseTime,
|
||||
data: &system.CombinedData{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SessionCache) Get(sessionID string) (stats *system.CombinedData, isCached bool) {
|
||||
if sessionID != c.primarySession && time.Since(c.lastUpdate) < c.leaseTime {
|
||||
return c.data, true
|
||||
}
|
||||
return c.data, false
|
||||
}
|
||||
|
||||
func (c *SessionCache) Set(sessionID string, data *system.CombinedData) {
|
||||
if data != nil {
|
||||
*c.data = *data
|
||||
}
|
||||
c.primarySession = sessionID
|
||||
c.lastUpdate = time.Now()
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSessionCache_GetSet(t *testing.T) {
|
||||
synctest.Run(func() {
|
||||
cache := NewSessionCache(69 * time.Second)
|
||||
|
||||
testData := &system.CombinedData{
|
||||
Info: system.Info{
|
||||
Hostname: "test-host",
|
||||
Cores: 4,
|
||||
},
|
||||
Stats: system.Stats{
|
||||
Cpu: 50.0,
|
||||
MemPct: 30.0,
|
||||
DiskPct: 40.0,
|
||||
},
|
||||
}
|
||||
|
||||
// Test initial state - should not be cached
|
||||
data, isCached := cache.Get("session1")
|
||||
assert.False(t, isCached, "Expected no cached data initially")
|
||||
assert.NotNil(t, data, "Expected data to be initialized")
|
||||
// Set data for session1
|
||||
cache.Set("session1", testData)
|
||||
|
||||
time.Sleep(15 * time.Second)
|
||||
|
||||
// Get data for a different session - should be cached
|
||||
data, isCached = cache.Get("session2")
|
||||
assert.True(t, isCached, "Expected data to be cached for non-primary session")
|
||||
require.NotNil(t, data, "Expected cached data to be returned")
|
||||
assert.Equal(t, "test-host", data.Info.Hostname, "Hostname should match test data")
|
||||
assert.Equal(t, 4, data.Info.Cores, "Cores should match test data")
|
||||
assert.Equal(t, 50.0, data.Stats.Cpu, "CPU should match test data")
|
||||
assert.Equal(t, 30.0, data.Stats.MemPct, "Memory percentage should match test data")
|
||||
assert.Equal(t, 40.0, data.Stats.DiskPct, "Disk percentage should match test data")
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
// Get data for the primary session - should not be cached
|
||||
data, isCached = cache.Get("session1")
|
||||
assert.False(t, isCached, "Expected data not to be cached for primary session")
|
||||
require.NotNil(t, data, "Expected data to be returned even if not cached")
|
||||
assert.Equal(t, "test-host", data.Info.Hostname, "Hostname should match test data")
|
||||
// if not cached, agent will update the data
|
||||
cache.Set("session1", testData)
|
||||
|
||||
time.Sleep(45 * time.Second)
|
||||
|
||||
// Get data for a different session - should still be cached
|
||||
_, isCached = cache.Get("session2")
|
||||
assert.True(t, isCached, "Expected data to be cached for non-primary session")
|
||||
|
||||
// Wait for the lease to expire
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// Get data for session2 - should not be cached
|
||||
_, isCached = cache.Get("session2")
|
||||
assert.False(t, isCached, "Expected data not to be cached after lease expiration")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSessionCache_NilData(t *testing.T) {
|
||||
// Create a new SessionCache
|
||||
cache := NewSessionCache(30 * time.Second)
|
||||
|
||||
// Test setting nil data (should not panic)
|
||||
assert.NotPanics(t, func() {
|
||||
cache.Set("session1", nil)
|
||||
}, "Setting nil data should not panic")
|
||||
|
||||
// Get data - should not be nil even though we set nil
|
||||
data, _ := cache.Get("session2")
|
||||
assert.NotNil(t, data, "Expected data to not be nil after setting nil data")
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -37,12 +36,7 @@ func (a *Agent) initializeDiskInfo() {
|
||||
|
||||
// Helper function to add a filesystem to fsStats if it doesn't exist
|
||||
addFsStat := func(device, mountpoint string, root bool) {
|
||||
var key string
|
||||
if runtime.GOOS == "windows" {
|
||||
key = device
|
||||
} else {
|
||||
key = filepath.Base(device)
|
||||
}
|
||||
key := filepath.Base(device)
|
||||
var ioMatch bool
|
||||
if _, exists := a.fsStats[key]; !exists {
|
||||
if root {
|
||||
|
||||
@@ -22,23 +22,10 @@ type dockerManager struct {
|
||||
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
|
||||
sem chan struct{} // Semaphore to limit concurrent container requests
|
||||
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
|
||||
apiContainerList []*container.ApiInfo // List of containers from Docker API (no pointer)
|
||||
apiContainerList *[]container.ApiInfo // List of containers from Docker API
|
||||
containerStatsMap map[string]*container.Stats // Keeps track of container stats
|
||||
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
|
||||
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
|
||||
isWindows bool // Whether the Docker Engine API is running on Windows
|
||||
}
|
||||
|
||||
// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests
|
||||
type userAgentRoundTripper struct {
|
||||
rt http.RoundTripper
|
||||
userAgent string
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface
|
||||
func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", u.userAgent)
|
||||
return u.rt.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Add goroutine to the queue
|
||||
@@ -65,14 +52,11 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
dm.apiContainerList = dm.apiContainerList[:0]
|
||||
if err := json.NewDecoder(resp.Body).Decode(&dm.apiContainerList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dm.isWindows = strings.Contains(resp.Header.Get("Server"), "windows")
|
||||
|
||||
containersLength := len(dm.apiContainerList)
|
||||
containersLength := len(*dm.apiContainerList)
|
||||
|
||||
// store valid ids to clean up old container ids from map
|
||||
if dm.validIds == nil {
|
||||
@@ -81,9 +65,9 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
||||
clear(dm.validIds)
|
||||
}
|
||||
|
||||
var failedContainers []*container.ApiInfo
|
||||
var failedContainters []container.ApiInfo
|
||||
|
||||
for _, ctr := range dm.apiContainerList {
|
||||
for _, ctr := range *dm.apiContainerList {
|
||||
ctr.IdShort = ctr.Id[:12]
|
||||
dm.validIds[ctr.IdShort] = struct{}{}
|
||||
// check if container is less than 1 minute old (possible restart)
|
||||
@@ -100,7 +84,7 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
||||
if err != nil {
|
||||
dm.containerStatsMutex.Lock()
|
||||
delete(dm.containerStatsMap, ctr.IdShort)
|
||||
failedContainers = append(failedContainers, ctr)
|
||||
failedContainters = append(failedContainters, ctr)
|
||||
dm.containerStatsMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
@@ -109,9 +93,9 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
||||
dm.wg.Wait()
|
||||
|
||||
// retry failed containers separately so we can run them in parallel (docker 24 bug)
|
||||
if len(failedContainers) > 0 {
|
||||
slog.Debug("Retrying failed containers", "count", len(failedContainers))
|
||||
for _, ctr := range failedContainers {
|
||||
if len(failedContainters) > 0 {
|
||||
slog.Debug("Retrying failed containers", "count", len(failedContainters))
|
||||
for _, ctr := range failedContainters {
|
||||
dm.queue()
|
||||
go func() {
|
||||
defer dm.dequeue()
|
||||
@@ -138,7 +122,7 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
||||
}
|
||||
|
||||
// Updates stats for individual container
|
||||
func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo) error {
|
||||
func (dm *dockerManager) updateContainerStats(ctr container.ApiInfo) error {
|
||||
name := ctr.Names[0][1:]
|
||||
|
||||
resp, err := dm.client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
||||
@@ -169,27 +153,22 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// calculate cpu and memory stats
|
||||
var usedMemory uint64
|
||||
var cpuPct float64
|
||||
|
||||
if dm.isWindows {
|
||||
usedMemory = res.MemoryStats.PrivateWorkingSet
|
||||
cpuPct = res.CalculateCpuPercentWindows(stats.PrevCpu[0], stats.PrevRead)
|
||||
} else {
|
||||
// check if container has valid data, otherwise may be in restart loop (#103)
|
||||
if res.MemoryStats.Usage == 0 {
|
||||
return fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name)
|
||||
}
|
||||
memCache := res.MemoryStats.Stats.InactiveFile
|
||||
if memCache == 0 {
|
||||
memCache = res.MemoryStats.Stats.Cache
|
||||
}
|
||||
usedMemory = res.MemoryStats.Usage - memCache
|
||||
|
||||
cpuPct = res.CalculateCpuPercentLinux(stats.PrevCpu)
|
||||
// check if container has valid data, otherwise may be in restart loop (#103)
|
||||
if res.MemoryStats.Usage == 0 {
|
||||
return fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name)
|
||||
}
|
||||
|
||||
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
|
||||
memCache := res.MemoryStats.Stats.InactiveFile
|
||||
if memCache == 0 {
|
||||
memCache = res.MemoryStats.Stats.Cache
|
||||
}
|
||||
usedMemory := res.MemoryStats.Usage - memCache
|
||||
|
||||
// cpu
|
||||
cpuDelta := res.CPUStats.CPUUsage.TotalUsage - stats.PrevCpu[0]
|
||||
systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1]
|
||||
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
||||
if cpuPct > 100 {
|
||||
return fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
||||
}
|
||||
@@ -204,18 +183,18 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo) error {
|
||||
var sent_delta, recv_delta float64
|
||||
// prevent first run from sending all prev sent/recv bytes
|
||||
if initialized {
|
||||
secondsElapsed := time.Since(stats.PrevRead).Seconds()
|
||||
secondsElapsed := time.Since(stats.PrevNet.Time).Seconds()
|
||||
sent_delta = float64(total_sent-stats.PrevNet.Sent) / secondsElapsed
|
||||
recv_delta = float64(total_recv-stats.PrevNet.Recv) / secondsElapsed
|
||||
}
|
||||
stats.PrevNet.Sent = total_sent
|
||||
stats.PrevNet.Recv = total_recv
|
||||
stats.PrevNet.Time = time.Now()
|
||||
|
||||
stats.Cpu = twoDecimals(cpuPct)
|
||||
stats.Mem = bytesToMegabytes(float64(usedMemory))
|
||||
stats.NetworkSent = bytesToMegabytes(sent_delta)
|
||||
stats.NetworkRecv = bytesToMegabytes(recv_delta)
|
||||
stats.PrevRead = res.Read
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -272,27 +251,20 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
slog.Info("DOCKER_TIMEOUT", "timeout", timeout)
|
||||
}
|
||||
|
||||
// Custom user-agent to avoid docker bug: https://github.com/docker/for-mac/issues/7575
|
||||
userAgentTransport := &userAgentRoundTripper{
|
||||
rt: transport,
|
||||
userAgent: "Docker-Client/",
|
||||
}
|
||||
|
||||
manager := &dockerManager{
|
||||
dockerClient := &dockerManager{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: userAgentTransport,
|
||||
Transport: transport,
|
||||
},
|
||||
containerStatsMap: make(map[string]*container.Stats),
|
||||
sem: make(chan struct{}, 5),
|
||||
apiContainerList: []*container.ApiInfo{},
|
||||
}
|
||||
|
||||
// If using podman, return client
|
||||
if strings.Contains(dockerHost, "podman") {
|
||||
a.systemInfo.Podman = true
|
||||
manager.goodDockerVersion = true
|
||||
return manager
|
||||
dockerClient.goodDockerVersion = true
|
||||
return dockerClient
|
||||
}
|
||||
|
||||
// Check docker version
|
||||
@@ -300,24 +272,23 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
var versionInfo struct {
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
resp, err := manager.client.Get("http://localhost/version")
|
||||
resp, err := dockerClient.client.Get("http://localhost/version")
|
||||
if err != nil {
|
||||
return manager
|
||||
return dockerClient
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&versionInfo); err != nil {
|
||||
return manager
|
||||
return dockerClient
|
||||
}
|
||||
|
||||
// if version > 24, one-shot works correctly and we can limit concurrent operations
|
||||
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
|
||||
manager.goodDockerVersion = true
|
||||
dockerClient.goodDockerVersion = true
|
||||
} else {
|
||||
slog.Info(fmt.Sprintf("Docker %s is outdated. Upgrade if possible. See https://github.com/henrygd/beszel/issues/58", versionInfo.Version))
|
||||
}
|
||||
|
||||
return manager
|
||||
return dockerClient
|
||||
}
|
||||
|
||||
// Test docker / podman sockets and return if one exists
|
||||
|
||||
@@ -3,7 +3,6 @@ package agent
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
@@ -16,28 +15,6 @@ import (
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
// Commands
|
||||
nvidiaSmiCmd = "nvidia-smi"
|
||||
rocmSmiCmd = "rocm-smi"
|
||||
tegraStatsCmd = "tegrastats"
|
||||
|
||||
// Polling intervals
|
||||
nvidiaSmiInterval = "4" // in seconds
|
||||
tegraStatsInterval = "3700" // in milliseconds
|
||||
rocmSmiInterval = 4300 * time.Millisecond
|
||||
|
||||
// Command retry and timeout constants
|
||||
retryWaitTime = 5 * time.Second
|
||||
maxFailureRetries = 5
|
||||
|
||||
cmdBufferSize = 10 * 1024
|
||||
|
||||
// Unit Conversions
|
||||
mebibytesInAMegabyte = 1.024 // nvidia-smi reports memory in MiB
|
||||
milliwattsInAWatt = 1000.0 // tegrastats reports power in mW
|
||||
)
|
||||
|
||||
// GPUManager manages data collection for GPUs (either Nvidia or AMD)
|
||||
type GPUManager struct {
|
||||
sync.Mutex
|
||||
@@ -79,7 +56,7 @@ func (c *gpuCollector) start() {
|
||||
break
|
||||
}
|
||||
slog.Warn(c.name+" failed, restarting", "err", err)
|
||||
time.Sleep(retryWaitTime)
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -98,7 +75,7 @@ func (c *gpuCollector) collect() error {
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
if c.buf == nil {
|
||||
c.buf = make([]byte, 0, cmdBufferSize)
|
||||
c.buf = make([]byte, 0, 4*1024)
|
||||
}
|
||||
scanner.Buffer(c.buf, bufio.MaxScanTokenSize)
|
||||
|
||||
@@ -125,35 +102,36 @@ func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
|
||||
// TODO: Maybe use VDD_IN for Nano / NX and add a total system power chart
|
||||
powerPattern := regexp.MustCompile(`(GPU_SOC|CPU_GPU_CV) (\d+)mW`)
|
||||
|
||||
// jetson devices have only one gpu so we'll just initialize here
|
||||
gpuData := &system.GPUData{Name: "GPU"}
|
||||
gm.GpuDataMap["0"] = gpuData
|
||||
|
||||
return func(output []byte) bool {
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
// we get gpu name from the intitial run of nvidia-smi, so return if it hasn't been initialized
|
||||
gpuData, ok := gm.GpuDataMap["0"]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
data := string(output)
|
||||
// Parse RAM usage
|
||||
ramMatches := ramPattern.FindSubmatch(output)
|
||||
ramMatches := ramPattern.FindStringSubmatch(data)
|
||||
if ramMatches != nil {
|
||||
gpuData.MemoryUsed, _ = strconv.ParseFloat(string(ramMatches[1]), 64)
|
||||
gpuData.MemoryTotal, _ = strconv.ParseFloat(string(ramMatches[2]), 64)
|
||||
gpuData.MemoryUsed, _ = strconv.ParseFloat(ramMatches[1], 64)
|
||||
gpuData.MemoryTotal, _ = strconv.ParseFloat(ramMatches[2], 64)
|
||||
}
|
||||
// Parse GR3D (GPU) usage
|
||||
gr3dMatches := gr3dPattern.FindSubmatch(output)
|
||||
gr3dMatches := gr3dPattern.FindStringSubmatch(data)
|
||||
if gr3dMatches != nil {
|
||||
gr3dUsage, _ := strconv.ParseFloat(string(gr3dMatches[1]), 64)
|
||||
gpuData.Usage += gr3dUsage
|
||||
gpuData.Usage, _ = strconv.ParseFloat(gr3dMatches[1], 64)
|
||||
}
|
||||
// Parse temperature
|
||||
tempMatches := tempPattern.FindSubmatch(output)
|
||||
tempMatches := tempPattern.FindStringSubmatch(data)
|
||||
if tempMatches != nil {
|
||||
gpuData.Temperature, _ = strconv.ParseFloat(string(tempMatches[1]), 64)
|
||||
gpuData.Temperature, _ = strconv.ParseFloat(tempMatches[1], 64)
|
||||
}
|
||||
// Parse power usage
|
||||
powerMatches := powerPattern.FindSubmatch(output)
|
||||
powerMatches := powerPattern.FindStringSubmatch(data)
|
||||
if powerMatches != nil {
|
||||
power, _ := strconv.ParseFloat(string(powerMatches[2]), 64)
|
||||
gpuData.Power += power / milliwattsInAWatt
|
||||
power, _ := strconv.ParseFloat(powerMatches[2], 64)
|
||||
gpuData.Power = power / 1000
|
||||
}
|
||||
gpuData.Count++
|
||||
return true
|
||||
@@ -164,10 +142,8 @@ func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
|
||||
func (gm *GPUManager) parseNvidiaData(output []byte) bool {
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
scanner := bufio.NewScanner(bytes.NewReader(output))
|
||||
var valid bool
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text() // Or use scanner.Bytes() for []byte
|
||||
for line := range strings.Lines(string(output)) {
|
||||
fields := strings.Split(strings.TrimSpace(line), ", ")
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
@@ -183,12 +159,18 @@ func (gm *GPUManager) parseNvidiaData(output []byte) bool {
|
||||
if _, ok := gm.GpuDataMap[id]; !ok {
|
||||
name := strings.TrimPrefix(fields[1], "NVIDIA ")
|
||||
gm.GpuDataMap[id] = &system.GPUData{Name: strings.TrimSuffix(name, " Laptop GPU")}
|
||||
// check if tegrastats is active - if so we will only use nvidia-smi to get gpu name
|
||||
// - nvidia-smi does not provide metrics for tegra / jetson devices
|
||||
// this will end the nvidia-smi collector
|
||||
if gm.tegrastats {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// update gpu data
|
||||
gpu := gm.GpuDataMap[id]
|
||||
gpu.Temperature = temp
|
||||
gpu.MemoryUsed = memoryUsage / mebibytesInAMegabyte
|
||||
gpu.MemoryTotal = totalMemory / mebibytesInAMegabyte
|
||||
gpu.MemoryUsed = memoryUsage / 1.024
|
||||
gpu.MemoryTotal = totalMemory / 1.024
|
||||
gpu.Usage += usage
|
||||
gpu.Power += power
|
||||
gpu.Count++
|
||||
@@ -259,7 +241,6 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
|
||||
}
|
||||
gpuData[id] = gpuCopy
|
||||
}
|
||||
slog.Debug("GPU", "data", gpuData)
|
||||
return gpuData
|
||||
}
|
||||
|
||||
@@ -268,15 +249,14 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
|
||||
// tools are found. If none of the tools are found, it returns an error indicating that no GPU
|
||||
// management tools are available.
|
||||
func (gm *GPUManager) detectGPUs() error {
|
||||
if _, err := exec.LookPath(nvidiaSmiCmd); err == nil {
|
||||
if _, err := exec.LookPath("nvidia-smi"); err == nil {
|
||||
gm.nvidiaSmi = true
|
||||
}
|
||||
if _, err := exec.LookPath(rocmSmiCmd); err == nil {
|
||||
if _, err := exec.LookPath("rocm-smi"); err == nil {
|
||||
gm.rocmSmi = true
|
||||
}
|
||||
if _, err := exec.LookPath(tegraStatsCmd); err == nil {
|
||||
if _, err := exec.LookPath("tegrastats"); err == nil {
|
||||
gm.tegrastats = true
|
||||
gm.nvidiaSmi = false
|
||||
}
|
||||
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats {
|
||||
return nil
|
||||
@@ -290,19 +270,17 @@ func (gm *GPUManager) startCollector(command string) {
|
||||
name: command,
|
||||
}
|
||||
switch command {
|
||||
case nvidiaSmiCmd:
|
||||
collector.cmdArgs = []string{
|
||||
"-l", nvidiaSmiInterval,
|
||||
case "nvidia-smi":
|
||||
collector.cmdArgs = []string{"-l", "4",
|
||||
"--query-gpu=index,name,temperature.gpu,memory.used,memory.total,utilization.gpu,power.draw",
|
||||
"--format=csv,noheader,nounits",
|
||||
}
|
||||
"--format=csv,noheader,nounits"}
|
||||
collector.parse = gm.parseNvidiaData
|
||||
go collector.start()
|
||||
case tegraStatsCmd:
|
||||
collector.cmdArgs = []string{"--interval", tegraStatsInterval}
|
||||
case "tegrastats":
|
||||
collector.cmdArgs = []string{"--interval", "3000"}
|
||||
collector.parse = gm.getJetsonParser()
|
||||
go collector.start()
|
||||
case rocmSmiCmd:
|
||||
case "rocm-smi":
|
||||
collector.cmdArgs = []string{"--showid", "--showtemp", "--showuse", "--showpower", "--showproductname", "--showmeminfo", "vram", "--json"}
|
||||
collector.parse = gm.parseAmdData
|
||||
go func() {
|
||||
@@ -310,12 +288,12 @@ func (gm *GPUManager) startCollector(command string) {
|
||||
for {
|
||||
if err := collector.collect(); err != nil {
|
||||
failures++
|
||||
if failures > maxFailureRetries {
|
||||
if failures > 5 {
|
||||
break
|
||||
}
|
||||
slog.Warn("Error collecting AMD GPU data", "err", err)
|
||||
}
|
||||
time.Sleep(rocmSmiInterval)
|
||||
time.Sleep(4300 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -330,13 +308,13 @@ func NewGPUManager() (*GPUManager, error) {
|
||||
gm.GpuDataMap = make(map[string]*system.GPUData)
|
||||
|
||||
if gm.nvidiaSmi {
|
||||
gm.startCollector(nvidiaSmiCmd)
|
||||
gm.startCollector("nvidia-smi")
|
||||
}
|
||||
if gm.rocmSmi {
|
||||
gm.startCollector(rocmSmiCmd)
|
||||
gm.startCollector("rocm-smi")
|
||||
}
|
||||
if gm.tegrastats {
|
||||
gm.startCollector(tegraStatsCmd)
|
||||
gm.startCollector("tegrastats")
|
||||
}
|
||||
|
||||
return &gm, nil
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
@@ -46,52 +43,6 @@ func TestParseNvidiaData(t *testing.T) {
|
||||
},
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "more valid multi-gpu data",
|
||||
input: `0, NVIDIA A10, 45, 19676, 23028, 0, 58.98
|
||||
1, NVIDIA A10, 45, 19638, 23028, 0, 62.35
|
||||
2, NVIDIA A10, 44, 21700, 23028, 0, 59.57
|
||||
3, NVIDIA A10, 45, 18222, 23028, 0, 61.76`,
|
||||
wantData: map[string]system.GPUData{
|
||||
"0": {
|
||||
Name: "A10",
|
||||
Temperature: 45.0,
|
||||
MemoryUsed: 19676.0 / 1.024,
|
||||
MemoryTotal: 23028.0 / 1.024,
|
||||
Usage: 0.0,
|
||||
Power: 58.98,
|
||||
Count: 1,
|
||||
},
|
||||
"1": {
|
||||
Name: "A10",
|
||||
Temperature: 45.0,
|
||||
MemoryUsed: 19638.0 / 1.024,
|
||||
MemoryTotal: 23028.0 / 1.024,
|
||||
Usage: 0.0,
|
||||
Power: 62.35,
|
||||
Count: 1,
|
||||
},
|
||||
"2": {
|
||||
Name: "A10",
|
||||
Temperature: 44.0,
|
||||
MemoryUsed: 21700.0 / 1.024,
|
||||
MemoryTotal: 23028.0 / 1.024,
|
||||
Usage: 0.0,
|
||||
Power: 59.57,
|
||||
Count: 1,
|
||||
},
|
||||
"3": {
|
||||
Name: "A10",
|
||||
Temperature: 45.0,
|
||||
MemoryUsed: 18222.0 / 1.024,
|
||||
MemoryTotal: 23028.0 / 1.024,
|
||||
Usage: 0.0,
|
||||
Power: 61.76,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
@@ -251,13 +202,14 @@ func TestParseJetsonData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
gm *GPUManager
|
||||
wantMetrics *system.GPUData
|
||||
}{
|
||||
{
|
||||
name: "valid data",
|
||||
input: "11-14-2024 22:54:33 RAM 4300/30698MB GR3D_FREQ 45% tj@52.468C VDD_GPU_SOC 2171mW",
|
||||
input: "RAM 4300/30698MB GR3D_FREQ 45% tj@52.468C VDD_GPU_SOC 2171mW",
|
||||
wantMetrics: &system.GPUData{
|
||||
Name: "GPU",
|
||||
Name: "Jetson",
|
||||
MemoryUsed: 4300.0,
|
||||
MemoryTotal: 30698.0,
|
||||
Usage: 45.0,
|
||||
@@ -266,24 +218,11 @@ func TestParseJetsonData(t *testing.T) {
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "more valid data",
|
||||
input: "11-15-2024 08:38:09 RAM 6185/7620MB (lfb 8x2MB) SWAP 851/3810MB (cached 1MB) CPU [15%@729,11%@729,14%@729,13%@729,11%@729,8%@729] EMC_FREQ 43%@2133 GR3D_FREQ 63%@[621] NVDEC off NVJPG off NVJPG1 off VIC off OFA off APE 200 cpu@53.968C soc2@52.437C soc0@50.75C gpu@53.343C tj@53.968C soc1@51.656C VDD_IN 12479mW/12479mW VDD_CPU_GPU_CV 4667mW/4667mW VDD_SOC 2817mW/2817mW",
|
||||
wantMetrics: &system.GPUData{
|
||||
Name: "GPU",
|
||||
MemoryUsed: 6185.0,
|
||||
MemoryTotal: 7620.0,
|
||||
Usage: 63.0,
|
||||
Temperature: 53.968,
|
||||
Power: 4.667,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing temperature",
|
||||
input: "11-14-2024 22:54:33 RAM 4300/30698MB GR3D_FREQ 45% VDD_GPU_SOC 2171mW",
|
||||
input: "RAM 4300/30698MB GR3D_FREQ 45% VDD_GPU_SOC 2171mW",
|
||||
wantMetrics: &system.GPUData{
|
||||
Name: "GPU",
|
||||
Name: "Jetson",
|
||||
MemoryUsed: 4300.0,
|
||||
MemoryTotal: 30698.0,
|
||||
Usage: 45.0,
|
||||
@@ -291,18 +230,32 @@ func TestParseJetsonData(t *testing.T) {
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no gpu defined by nvidia-smi",
|
||||
input: "RAM 4300/30698MB GR3D_FREQ 45% VDD_GPU_SOC 2171mW",
|
||||
gm: &GPUManager{
|
||||
GpuDataMap: map[string]*system.GPUData{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
GpuDataMap: make(map[string]*system.GPUData),
|
||||
if tt.gm != nil {
|
||||
// should return if no gpu set by nvidia-smi
|
||||
assert.Empty(t, tt.gm.GpuDataMap)
|
||||
return
|
||||
}
|
||||
parser := gm.getJetsonParser()
|
||||
tt.gm = &GPUManager{
|
||||
GpuDataMap: map[string]*system.GPUData{
|
||||
"0": {Name: "Jetson"},
|
||||
},
|
||||
}
|
||||
parser := tt.gm.getJetsonParser()
|
||||
valid := parser([]byte(tt.input))
|
||||
assert.Equal(t, true, valid)
|
||||
|
||||
got := gm.GpuDataMap["0"]
|
||||
got := tt.gm.GpuDataMap["0"]
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, tt.wantMetrics.Name, got.Name)
|
||||
assert.InDelta(t, tt.wantMetrics.MemoryUsed, got.MemoryUsed, 0.01)
|
||||
@@ -428,7 +381,7 @@ echo "test"`
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantNvidiaSmi: false,
|
||||
wantNvidiaSmi: true,
|
||||
wantRocmSmi: true,
|
||||
wantTegrastats: true,
|
||||
wantErr: false,
|
||||
@@ -533,7 +486,7 @@ echo '{"card0": {"Temperature (Sensor edge) (C)": "49.0", "Current Socket Graphi
|
||||
setup: func(t *testing.T) error {
|
||||
path := filepath.Join(dir, "tegrastats")
|
||||
script := `#!/bin/sh
|
||||
echo "11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 80% tj@70C VDD_GPU_SOC 1000mW"`
|
||||
echo "RAM 1024/4096MB GR3D_FREQ 80% tj@70C VDD_GPU_SOC 1000mW"`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -570,158 +523,3 @@ echo "11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 80% tj@70C VDD_GPU_SOC 1000m
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccumulationTableDriven tests the accumulation behavior for all three GPU types
|
||||
func TestAccumulation(t *testing.T) {
|
||||
type expectedGPUValues struct {
|
||||
temperature float64
|
||||
memoryUsed float64
|
||||
memoryTotal float64
|
||||
usage float64
|
||||
power float64
|
||||
count float64
|
||||
avgUsage float64
|
||||
avgPower float64
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
initialGPUData map[string]*system.GPUData
|
||||
dataSamples [][]byte
|
||||
parser func(*GPUManager) func([]byte) bool
|
||||
expectedValues map[string]expectedGPUValues
|
||||
}{
|
||||
{
|
||||
name: "Jetson GPU accumulation",
|
||||
initialGPUData: map[string]*system.GPUData{
|
||||
"0": {
|
||||
Name: "Jetson",
|
||||
Temperature: 0,
|
||||
Usage: 0,
|
||||
Power: 0,
|
||||
Count: 0,
|
||||
},
|
||||
},
|
||||
dataSamples: [][]byte{
|
||||
[]byte("11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 30% tj@50.5C VDD_GPU_SOC 1000mW"),
|
||||
[]byte("11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 40% tj@60.5C VDD_GPU_SOC 1200mW"),
|
||||
[]byte("11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 50% tj@70.5C VDD_GPU_SOC 1400mW"),
|
||||
},
|
||||
parser: func(gm *GPUManager) func([]byte) bool {
|
||||
return gm.getJetsonParser()
|
||||
},
|
||||
expectedValues: map[string]expectedGPUValues{
|
||||
"0": {
|
||||
temperature: 70.5, // Last value
|
||||
memoryUsed: 1024, // Last value
|
||||
memoryTotal: 4096, // Last value
|
||||
usage: 120.0, // Accumulated: 30 + 40 + 50
|
||||
power: 3.6, // Accumulated: 1.0 + 1.2 + 1.4
|
||||
count: 3,
|
||||
avgUsage: 40.0, // 120 / 3
|
||||
avgPower: 1.2, // 3.6 / 3
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NVIDIA GPU accumulation",
|
||||
initialGPUData: map[string]*system.GPUData{
|
||||
// NVIDIA parser will create the GPU data entries
|
||||
},
|
||||
dataSamples: [][]byte{
|
||||
[]byte("0, NVIDIA GeForce RTX 3080, 50, 5000, 10000, 30, 200"),
|
||||
[]byte("0, NVIDIA GeForce RTX 3080, 60, 6000, 10000, 40, 250"),
|
||||
[]byte("0, NVIDIA GeForce RTX 3080, 70, 7000, 10000, 50, 300"),
|
||||
},
|
||||
parser: func(gm *GPUManager) func([]byte) bool {
|
||||
return gm.parseNvidiaData
|
||||
},
|
||||
expectedValues: map[string]expectedGPUValues{
|
||||
"0": {
|
||||
temperature: 70.0, // Last value
|
||||
memoryUsed: 7000.0 / 1.024, // Last value
|
||||
memoryTotal: 10000.0 / 1.024, // Last value
|
||||
usage: 120.0, // Accumulated: 30 + 40 + 50
|
||||
power: 750.0, // Accumulated: 200 + 250 + 300
|
||||
count: 3,
|
||||
avgUsage: 40.0, // 120 / 3
|
||||
avgPower: 250.0, // 750 / 3
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AMD GPU accumulation",
|
||||
initialGPUData: map[string]*system.GPUData{
|
||||
// AMD parser will create the GPU data entries
|
||||
},
|
||||
dataSamples: [][]byte{
|
||||
[]byte(`{"card0": {"GUID": "34756", "Temperature (Sensor edge) (C)": "50.0", "Current Socket Graphics Package Power (W)": "100.0", "GPU use (%)": "30", "VRAM Total Memory (B)": "10737418240", "VRAM Total Used Memory (B)": "1073741824", "Card Series": "Radeon RX 6800"}}`),
|
||||
[]byte(`{"card0": {"GUID": "34756", "Temperature (Sensor edge) (C)": "60.0", "Current Socket Graphics Package Power (W)": "150.0", "GPU use (%)": "40", "VRAM Total Memory (B)": "10737418240", "VRAM Total Used Memory (B)": "2147483648", "Card Series": "Radeon RX 6800"}}`),
|
||||
[]byte(`{"card0": {"GUID": "34756", "Temperature (Sensor edge) (C)": "70.0", "Current Socket Graphics Package Power (W)": "200.0", "GPU use (%)": "50", "VRAM Total Memory (B)": "10737418240", "VRAM Total Used Memory (B)": "3221225472", "Card Series": "Radeon RX 6800"}}`),
|
||||
},
|
||||
parser: func(gm *GPUManager) func([]byte) bool {
|
||||
return gm.parseAmdData
|
||||
},
|
||||
expectedValues: map[string]expectedGPUValues{
|
||||
"34756": {
|
||||
temperature: 70.0, // Last value
|
||||
memoryUsed: 3221225472.0 / (1024 * 1024), // Last value
|
||||
memoryTotal: 10737418240.0 / (1024 * 1024), // Last value
|
||||
usage: 120.0, // Accumulated: 30 + 40 + 50
|
||||
power: 450.0, // Accumulated: 100 + 150 + 200
|
||||
count: 3,
|
||||
avgUsage: 40.0, // 120 / 3
|
||||
avgPower: 150.0, // 450 / 3
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a new GPUManager for each test
|
||||
gm := &GPUManager{
|
||||
GpuDataMap: tt.initialGPUData,
|
||||
}
|
||||
|
||||
// Get the parser function
|
||||
parser := tt.parser(gm)
|
||||
|
||||
// Process each data sample
|
||||
for i, sample := range tt.dataSamples {
|
||||
valid := parser(sample)
|
||||
assert.True(t, valid, "Sample %d should be valid", i)
|
||||
}
|
||||
|
||||
// Check accumulated values
|
||||
for id, expected := range tt.expectedValues {
|
||||
gpu, exists := gm.GpuDataMap[id]
|
||||
assert.True(t, exists, "GPU with ID %s should exist", id)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.InDelta(t, expected.temperature, gpu.Temperature, 0.01, "Temperature should match")
|
||||
assert.InDelta(t, expected.memoryUsed, gpu.MemoryUsed, 0.01, "Memory used should match")
|
||||
assert.InDelta(t, expected.memoryTotal, gpu.MemoryTotal, 0.01, "Memory total should match")
|
||||
assert.InDelta(t, expected.usage, gpu.Usage, 0.01, "Usage should match")
|
||||
assert.InDelta(t, expected.power, gpu.Power, 0.01, "Power should match")
|
||||
assert.Equal(t, expected.count, gpu.Count, "Count should match")
|
||||
}
|
||||
|
||||
// Verify average calculation in GetCurrentData
|
||||
result := gm.GetCurrentData()
|
||||
for id, expected := range tt.expectedValues {
|
||||
gpu, exists := result[id]
|
||||
assert.True(t, exists, "GPU with ID %s should exist in GetCurrentData result", id)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.InDelta(t, expected.temperature, gpu.Temperature, 0.01, "Temperature in GetCurrentData should match")
|
||||
assert.InDelta(t, expected.avgUsage, gpu.Usage, 0.01, "Average usage in GetCurrentData should match")
|
||||
assert.InDelta(t, expected.avgPower, gpu.Power, 0.01, "Average power in GetCurrentData should match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Health checks if the agent's server is running by attempting to connect to it.
|
||||
//
|
||||
// If an error occurs when attempting to connect to the server, it returns the error.
|
||||
func Health(addr string, network string) error {
|
||||
conn, err := net.DialTimeout(network, addr, 4*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package agent_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"beszel/internal/agent"
|
||||
)
|
||||
|
||||
// setupTestServer creates a temporary server for testing
|
||||
func setupTestServer(t *testing.T) (string, func()) {
|
||||
// Create a temporary socket file for Unix socket testing
|
||||
tempSockFile := os.TempDir() + "/beszel_health_test.sock"
|
||||
|
||||
// Clean up any existing socket file
|
||||
os.Remove(tempSockFile)
|
||||
|
||||
// Create a listener
|
||||
listener, err := net.Listen("unix", tempSockFile)
|
||||
require.NoError(t, err, "Failed to create test listener")
|
||||
|
||||
// Start a simple server in a goroutine
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return // Listener closed
|
||||
}
|
||||
defer conn.Close()
|
||||
// Just accept the connection and do nothing
|
||||
}()
|
||||
|
||||
// Return the socket file path and a cleanup function
|
||||
return tempSockFile, func() {
|
||||
listener.Close()
|
||||
os.Remove(tempSockFile)
|
||||
}
|
||||
}
|
||||
|
||||
// setupTCPTestServer creates a temporary TCP server for testing
|
||||
func setupTCPTestServer(t *testing.T) (string, func()) {
|
||||
// Listen on a random available port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "Failed to create test listener")
|
||||
|
||||
// Get the port that was assigned
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
port := addr.Port
|
||||
|
||||
// Start a simple server in a goroutine
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return // Listener closed
|
||||
}
|
||||
defer conn.Close()
|
||||
// Just accept the connection and do nothing
|
||||
}()
|
||||
|
||||
// Return the address and a cleanup function
|
||||
return fmt.Sprintf("127.0.0.1:%d", port), func() {
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealth(t *testing.T) {
|
||||
t.Run("server is running (unix socket)", func(t *testing.T) {
|
||||
// Setup a test server
|
||||
sockFile, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Run the health check with explicit parameters
|
||||
err := agent.Health(sockFile, "unix")
|
||||
require.NoError(t, err, "Failed to check health")
|
||||
})
|
||||
|
||||
t.Run("server is running (tcp address)", func(t *testing.T) {
|
||||
// Setup a test server
|
||||
addr, cleanup := setupTCPTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Run the health check with explicit parameters
|
||||
err := agent.Health(addr, "tcp")
|
||||
require.NoError(t, err, "Failed to check health")
|
||||
})
|
||||
|
||||
t.Run("server is not running", func(t *testing.T) {
|
||||
// Use an address that's likely not in use
|
||||
addr := "127.0.0.1:65535"
|
||||
|
||||
// Run the health check with explicit parameters
|
||||
err := agent.Health(addr, "tcp")
|
||||
require.Error(t, err, "Health check should return an error when server is not running")
|
||||
})
|
||||
|
||||
t.Run("invalid network", func(t *testing.T) {
|
||||
// Use an invalid network type
|
||||
err := agent.Health("127.0.0.1:8080", "invalid_network")
|
||||
require.Error(t, err, "Health check should return an error with invalid network")
|
||||
})
|
||||
|
||||
t.Run("unix socket not found", func(t *testing.T) {
|
||||
// Use a non-existent unix socket
|
||||
nonExistentSocket := os.TempDir() + "/non_existent_socket.sock"
|
||||
|
||||
// Make sure it really doesn't exist
|
||||
os.Remove(nonExistentSocket)
|
||||
|
||||
err := agent.Health(nonExistentSocket, "unix")
|
||||
require.Error(t, err, "Health check should return an error when socket doesn't exist")
|
||||
})
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func (a *Agent) initializeNetIoStats() {
|
||||
nics, nicsEnvExists := GetEnv("NICS")
|
||||
if nicsEnvExists {
|
||||
nicsMap = make(map[string]struct{}, 0)
|
||||
for nic := range strings.SplitSeq(nics, ",") {
|
||||
for _, nic := range strings.Split(nics, ",") {
|
||||
nicsMap[nic] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"log/slog"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
type SensorConfig struct {
|
||||
context context.Context
|
||||
sensors map[string]struct{}
|
||||
primarySensor string
|
||||
isBlacklist bool
|
||||
hasWildcards bool
|
||||
skipCollection bool
|
||||
}
|
||||
|
||||
func (a *Agent) newSensorConfig() *SensorConfig {
|
||||
primarySensor, _ := GetEnv("PRIMARY_SENSOR")
|
||||
sysSensors, _ := GetEnv("SYS_SENSORS")
|
||||
sensorsEnvVal, sensorsSet := GetEnv("SENSORS")
|
||||
skipCollection := sensorsSet && sensorsEnvVal == ""
|
||||
|
||||
return a.newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, skipCollection)
|
||||
}
|
||||
|
||||
// newSensorConfigWithEnv creates a SensorConfig with the provided environment variables
|
||||
// sensorsSet indicates if the SENSORS environment variable was explicitly set (even to empty string)
|
||||
func (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal string, skipCollection bool) *SensorConfig {
|
||||
config := &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: primarySensor,
|
||||
skipCollection: skipCollection,
|
||||
sensors: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
// Set sensors context (allows overriding sys location for sensors)
|
||||
if sysSensors != "" {
|
||||
slog.Info("SYS_SENSORS", "path", sysSensors)
|
||||
config.context = context.WithValue(config.context,
|
||||
common.EnvKey, common.EnvMap{common.HostSysEnvKey: sysSensors},
|
||||
)
|
||||
}
|
||||
|
||||
// handle blacklist
|
||||
if strings.HasPrefix(sensorsEnvVal, "-") {
|
||||
config.isBlacklist = true
|
||||
sensorsEnvVal = sensorsEnvVal[1:]
|
||||
}
|
||||
|
||||
for sensor := range strings.SplitSeq(sensorsEnvVal, ",") {
|
||||
sensor = strings.TrimSpace(sensor)
|
||||
if sensor != "" {
|
||||
config.sensors[sensor] = struct{}{}
|
||||
if strings.Contains(sensor, "*") {
|
||||
config.hasWildcards = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// updateTemperatures updates the agent with the latest sensor temperatures
|
||||
func (a *Agent) updateTemperatures(systemStats *system.Stats) {
|
||||
// skip if sensors whitelist is set to empty string
|
||||
if a.sensorConfig.skipCollection {
|
||||
slog.Debug("Skipping temperature collection")
|
||||
return
|
||||
}
|
||||
|
||||
// reset high temp
|
||||
a.systemInfo.DashboardTemp = 0
|
||||
|
||||
// get sensor data
|
||||
temps, _ := sensors.TemperaturesWithContext(a.sensorConfig.context)
|
||||
slog.Debug("Temperature", "sensors", temps)
|
||||
|
||||
// return if no sensors
|
||||
if len(temps) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
systemStats.Temperatures = make(map[string]float64, len(temps))
|
||||
for i, sensor := range temps {
|
||||
// skip if temperature is unreasonable
|
||||
if sensor.Temperature <= 0 || sensor.Temperature >= 200 {
|
||||
continue
|
||||
}
|
||||
sensorName := sensor.SensorKey
|
||||
if _, ok := systemStats.Temperatures[sensorName]; ok {
|
||||
// if key already exists, append int to key
|
||||
sensorName = sensorName + "_" + strconv.Itoa(i)
|
||||
}
|
||||
// skip if not in whitelist or blacklist
|
||||
if !isValidSensor(sensorName, a.sensorConfig) {
|
||||
continue
|
||||
}
|
||||
// set dashboard temperature
|
||||
if a.sensorConfig.primarySensor == "" {
|
||||
a.systemInfo.DashboardTemp = max(a.systemInfo.DashboardTemp, sensor.Temperature)
|
||||
} else if a.sensorConfig.primarySensor == sensorName {
|
||||
a.systemInfo.DashboardTemp = sensor.Temperature
|
||||
}
|
||||
systemStats.Temperatures[sensorName] = twoDecimals(sensor.Temperature)
|
||||
}
|
||||
}
|
||||
|
||||
// isValidSensor checks if a sensor is valid based on the sensor name and the sensor config
|
||||
func isValidSensor(sensorName string, config *SensorConfig) bool {
|
||||
// if no sensors configured, everything is valid
|
||||
if len(config.sensors) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Exact match - return true if whitelist, false if blacklist
|
||||
if _, exactMatch := config.sensors[sensorName]; exactMatch {
|
||||
return !config.isBlacklist
|
||||
}
|
||||
|
||||
// If no wildcards, return true if blacklist, false if whitelist
|
||||
if !config.hasWildcards {
|
||||
return config.isBlacklist
|
||||
}
|
||||
|
||||
// Check for wildcard patterns
|
||||
for pattern := range config.sensors {
|
||||
if !strings.Contains(pattern, "*") {
|
||||
continue
|
||||
}
|
||||
if match, _ := path.Match(pattern, sensorName); match {
|
||||
return !config.isBlacklist
|
||||
}
|
||||
}
|
||||
|
||||
return config.isBlacklist
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsValidSensor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sensorName string
|
||||
config *SensorConfig
|
||||
expectedValid bool
|
||||
}{
|
||||
{
|
||||
name: "Whitelist - sensor in list",
|
||||
sensorName: "cpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}},
|
||||
isBlacklist: false,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist - sensor not in list",
|
||||
sensorName: "gpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}},
|
||||
isBlacklist: false,
|
||||
},
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist - sensor in list",
|
||||
sensorName: "cpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}},
|
||||
isBlacklist: true,
|
||||
},
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist - sensor not in list",
|
||||
sensorName: "gpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}},
|
||||
isBlacklist: true,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist with wildcard - matching pattern",
|
||||
sensorName: "core_0_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"core_*_temp": {}},
|
||||
isBlacklist: false,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist with wildcard - non-matching pattern",
|
||||
sensorName: "gpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"core_*_temp": {}},
|
||||
isBlacklist: false,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist with wildcard - matching pattern",
|
||||
sensorName: "core_0_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"core_*_temp": {}},
|
||||
isBlacklist: true,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist with wildcard - non-matching pattern",
|
||||
sensorName: "gpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"core_*_temp": {}},
|
||||
isBlacklist: true,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "No sensors configured",
|
||||
sensorName: "any_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{},
|
||||
isBlacklist: false,
|
||||
hasWildcards: false,
|
||||
skipCollection: false,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "Mixed patterns in whitelist - exact match",
|
||||
sensorName: "cpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
||||
isBlacklist: false,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "Mixed patterns in whitelist - wildcard match",
|
||||
sensorName: "core_1_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
||||
isBlacklist: false,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "Mixed patterns in blacklist - exact match",
|
||||
sensorName: "cpu_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
||||
isBlacklist: true,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "Mixed patterns in blacklist - wildcard match",
|
||||
sensorName: "core_1_temp",
|
||||
config: &SensorConfig{
|
||||
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
||||
isBlacklist: true,
|
||||
hasWildcards: true,
|
||||
},
|
||||
expectedValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isValidSensor(tt.sensorName, tt.config)
|
||||
assert.Equal(t, tt.expectedValid, result, "isValidSensor(%q, config) returned unexpected result", tt.sensorName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSensorConfigWithEnv(t *testing.T) {
|
||||
agent := &Agent{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
primarySensor string
|
||||
sysSensors string
|
||||
sensors string
|
||||
skipCollection bool
|
||||
expectedConfig *SensorConfig
|
||||
}{
|
||||
{
|
||||
name: "Empty configuration",
|
||||
primarySensor: "",
|
||||
sysSensors: "",
|
||||
sensors: "",
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "",
|
||||
sensors: map[string]struct{}{},
|
||||
isBlacklist: false,
|
||||
hasWildcards: false,
|
||||
skipCollection: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Explicitly set to empty string",
|
||||
primarySensor: "",
|
||||
sysSensors: "",
|
||||
sensors: "",
|
||||
skipCollection: true,
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "",
|
||||
sensors: map[string]struct{}{},
|
||||
isBlacklist: false,
|
||||
hasWildcards: false,
|
||||
skipCollection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Primary sensor only - should create sensor map",
|
||||
primarySensor: "cpu_temp",
|
||||
sysSensors: "",
|
||||
sensors: "",
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "cpu_temp",
|
||||
sensors: map[string]struct{}{},
|
||||
isBlacklist: false,
|
||||
hasWildcards: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Whitelist sensors",
|
||||
primarySensor: "cpu_temp",
|
||||
sysSensors: "",
|
||||
sensors: "cpu_temp,gpu_temp",
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "cpu_temp",
|
||||
sensors: map[string]struct{}{
|
||||
"cpu_temp": {},
|
||||
"gpu_temp": {},
|
||||
},
|
||||
isBlacklist: false,
|
||||
hasWildcards: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Blacklist sensors",
|
||||
primarySensor: "cpu_temp",
|
||||
sysSensors: "",
|
||||
sensors: "-cpu_temp,gpu_temp",
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "cpu_temp",
|
||||
sensors: map[string]struct{}{
|
||||
"cpu_temp": {},
|
||||
"gpu_temp": {},
|
||||
},
|
||||
isBlacklist: true,
|
||||
hasWildcards: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sensors with wildcard",
|
||||
primarySensor: "cpu_temp",
|
||||
sysSensors: "",
|
||||
sensors: "cpu_*,gpu_temp",
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "cpu_temp",
|
||||
sensors: map[string]struct{}{
|
||||
"cpu_*": {},
|
||||
"gpu_temp": {},
|
||||
},
|
||||
isBlacklist: false,
|
||||
hasWildcards: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sensors with whitespace",
|
||||
primarySensor: "cpu_temp",
|
||||
sysSensors: "",
|
||||
sensors: "cpu_*, gpu_temp",
|
||||
expectedConfig: &SensorConfig{
|
||||
context: context.Background(),
|
||||
primarySensor: "cpu_temp",
|
||||
sensors: map[string]struct{}{
|
||||
"cpu_*": {},
|
||||
"gpu_temp": {},
|
||||
},
|
||||
isBlacklist: false,
|
||||
hasWildcards: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With SYS_SENSORS path",
|
||||
primarySensor: "cpu_temp",
|
||||
sysSensors: "/custom/path",
|
||||
sensors: "cpu_temp",
|
||||
expectedConfig: &SensorConfig{
|
||||
primarySensor: "cpu_temp",
|
||||
sensors: map[string]struct{}{
|
||||
"cpu_temp": {},
|
||||
},
|
||||
isBlacklist: false,
|
||||
hasWildcards: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.skipCollection)
|
||||
|
||||
// Check primary sensor
|
||||
assert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor)
|
||||
|
||||
// Check sensor map
|
||||
if tt.expectedConfig.sensors == nil {
|
||||
assert.Nil(t, result.sensors)
|
||||
} else {
|
||||
assert.Equal(t, len(tt.expectedConfig.sensors), len(result.sensors))
|
||||
for sensor := range tt.expectedConfig.sensors {
|
||||
_, exists := result.sensors[sensor]
|
||||
assert.True(t, exists, "Sensor %s should exist in the result", sensor)
|
||||
}
|
||||
}
|
||||
|
||||
// Check flags
|
||||
assert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist)
|
||||
assert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards)
|
||||
|
||||
// Check context
|
||||
if tt.sysSensors != "" {
|
||||
// Verify context contains correct values
|
||||
envMap, ok := result.context.Value(common.EnvKey).(common.EnvMap)
|
||||
require.True(t, ok, "Context should contain EnvMap")
|
||||
sysPath, ok := envMap[common.HostSysEnvKey]
|
||||
require.True(t, ok, "EnvMap should contain HostSysEnvKey")
|
||||
assert.Equal(t, tt.sysSensors, sysPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSensorConfig(t *testing.T) {
|
||||
// Save original environment variables
|
||||
originalPrimary, hasPrimary := os.LookupEnv("BESZEL_AGENT_PRIMARY_SENSOR")
|
||||
originalSys, hasSys := os.LookupEnv("BESZEL_AGENT_SYS_SENSORS")
|
||||
originalSensors, hasSensors := os.LookupEnv("BESZEL_AGENT_SENSORS")
|
||||
|
||||
// Restore environment variables after the test
|
||||
defer func() {
|
||||
// Clean up test environment variables
|
||||
os.Unsetenv("BESZEL_AGENT_PRIMARY_SENSOR")
|
||||
os.Unsetenv("BESZEL_AGENT_SYS_SENSORS")
|
||||
os.Unsetenv("BESZEL_AGENT_SENSORS")
|
||||
|
||||
// Restore original values if they existed
|
||||
if hasPrimary {
|
||||
os.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", originalPrimary)
|
||||
}
|
||||
if hasSys {
|
||||
os.Setenv("BESZEL_AGENT_SYS_SENSORS", originalSys)
|
||||
}
|
||||
if hasSensors {
|
||||
os.Setenv("BESZEL_AGENT_SENSORS", originalSensors)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set test environment variables
|
||||
os.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary")
|
||||
os.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path")
|
||||
os.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3")
|
||||
|
||||
agent := &Agent{}
|
||||
result := agent.newSensorConfig()
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "test_primary", result.primarySensor)
|
||||
assert.NotNil(t, result.sensors)
|
||||
assert.Equal(t, 3, len(result.sensors))
|
||||
assert.True(t, result.hasWildcards)
|
||||
assert.False(t, result.isBlacklist)
|
||||
|
||||
// Check that sys sensors path is in context
|
||||
envMap, ok := result.context.Value(common.EnvKey).(common.EnvMap)
|
||||
require.True(t, ok, "Context should contain EnvMap")
|
||||
sysPath, ok := envMap[common.HostSysEnvKey]
|
||||
require.True(t, ok, "EnvMap should contain HostSysEnvKey")
|
||||
assert.Equal(t, "/test/path", sysPath)
|
||||
}
|
||||
@@ -23,14 +23,20 @@ func (a *Agent) StartServer(opts ServerOptions) error {
|
||||
|
||||
slog.Info("Starting SSH server", "addr", opts.Addr, "network", opts.Network)
|
||||
|
||||
if opts.Network == "unix" {
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
// remove existing socket file if it exists
|
||||
if err := os.Remove(opts.Addr); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// prefix with : if only port was provided
|
||||
if !strings.Contains(opts.Addr, ":") {
|
||||
opts.Addr = ":" + opts.Addr
|
||||
}
|
||||
}
|
||||
|
||||
// start listening on the address
|
||||
// Listen on the address
|
||||
ln, err := net.Listen(opts.Network, opts.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -38,7 +44,7 @@ func (a *Agent) StartServer(opts ServerOptions) error {
|
||||
defer ln.Close()
|
||||
|
||||
// Start SSH server on the listener
|
||||
return sshServer.Serve(ln, nil, sshServer.NoPty(),
|
||||
err = sshServer.Serve(ln, nil, sshServer.NoPty(),
|
||||
sshServer.PublicKeyAuth(func(ctx sshServer.Context, key sshServer.PublicKey) bool {
|
||||
for _, pubKey := range opts.Keys {
|
||||
if sshServer.KeysEqual(key, pubKey) {
|
||||
@@ -48,11 +54,15 @@ func (a *Agent) StartServer(opts ServerOptions) error {
|
||||
return false
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) handleSession(s sshServer.Session) {
|
||||
slog.Debug("New session", "client", s.RemoteAddr())
|
||||
stats := a.gatherStats(s.Context().SessionID())
|
||||
// slog.Debug("connection", "remoteaddr", s.RemoteAddr(), "user", s.User())
|
||||
stats := a.gatherStats()
|
||||
if err := json.NewEncoder(s).Encode(stats); err != nil {
|
||||
slog.Error("Error encoding stats", "err", err, "stats", stats)
|
||||
s.Exit(1)
|
||||
@@ -64,48 +74,24 @@ func (a *Agent) handleSession(s sshServer.Session) {
|
||||
// It returns a slice of ssh.PublicKey and an error if any key fails to parse.
|
||||
func ParseKeys(input string) ([]ssh.PublicKey, error) {
|
||||
var parsedKeys []ssh.PublicKey
|
||||
|
||||
for line := range strings.Lines(input) {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines or comments
|
||||
if len(line) == 0 || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the key
|
||||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse key: %s, error: %w", line, err)
|
||||
}
|
||||
|
||||
// Append the parsed key to the list
|
||||
parsedKeys = append(parsedKeys, parsedKey)
|
||||
}
|
||||
|
||||
return parsedKeys, nil
|
||||
}
|
||||
|
||||
// GetAddress gets the address to listen on or connect to from environment variables or default value.
|
||||
func GetAddress(addr string) string {
|
||||
if addr == "" {
|
||||
addr, _ = GetEnv("LISTEN")
|
||||
}
|
||||
if addr == "" {
|
||||
// Legacy PORT environment variable support
|
||||
addr, _ = GetEnv("PORT")
|
||||
}
|
||||
if addr == "" {
|
||||
return ":45876"
|
||||
}
|
||||
// prefix with : if only port was provided
|
||||
if GetNetwork(addr) != "unix" && !strings.Contains(addr, ":") {
|
||||
addr = ":" + addr
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// GetNetwork returns the network type to use based on the address
|
||||
func GetNetwork(addr string) string {
|
||||
if network, ok := GetEnv("NETWORK"); ok && network != "" {
|
||||
return network
|
||||
}
|
||||
if strings.HasPrefix(addr, "/") {
|
||||
return "unix"
|
||||
}
|
||||
return "tcp"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestStartServer(t *testing.T) {
|
||||
name: "tcp port only",
|
||||
config: ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: ":45987",
|
||||
Addr: "45987",
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
},
|
||||
@@ -88,7 +88,7 @@ func TestStartServer(t *testing.T) {
|
||||
name: "bad key should fail",
|
||||
config: ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: ":45987",
|
||||
Addr: "45987",
|
||||
Keys: []ssh.PublicKey{sshBadPubKey},
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -98,7 +98,7 @@ func TestStartServer(t *testing.T) {
|
||||
name: "good key still good",
|
||||
config: ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: ":45987",
|
||||
Addr: "45987",
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,28 +16,14 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
// Sets initial / non-changing values about the host system
|
||||
func (a *Agent) initializeSystemInfo() {
|
||||
a.systemInfo.AgentVersion = beszel.Version
|
||||
a.systemInfo.Hostname, _ = os.Hostname()
|
||||
|
||||
platform, _, version, _ := host.PlatformInformation()
|
||||
|
||||
if platform == "darwin" {
|
||||
a.systemInfo.KernelVersion = version
|
||||
a.systemInfo.Os = system.Darwin
|
||||
} else if strings.Contains(platform, "indows") {
|
||||
a.systemInfo.KernelVersion = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
|
||||
a.systemInfo.Os = system.Windows
|
||||
} else {
|
||||
a.systemInfo.Os = system.Linux
|
||||
}
|
||||
|
||||
if a.systemInfo.KernelVersion == "" {
|
||||
a.systemInfo.KernelVersion, _ = host.KernelVersion()
|
||||
}
|
||||
a.systemInfo.KernelVersion, _ = host.KernelVersion()
|
||||
|
||||
// cpu model
|
||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||
@@ -198,9 +184,11 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
}
|
||||
}
|
||||
|
||||
// temperatures
|
||||
// TODO: maybe refactor to methods on systemStats
|
||||
a.updateTemperatures(&systemStats)
|
||||
// temperatures (skip if sensors whitelist is set to empty string)
|
||||
err = a.updateTemperatures(&systemStats)
|
||||
if err != nil {
|
||||
slog.Error("Error getting temperatures", "err", err)
|
||||
}
|
||||
|
||||
// GPU data
|
||||
if a.gpuManager != nil {
|
||||
@@ -214,24 +202,13 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
if systemStats.Temperatures == nil {
|
||||
systemStats.Temperatures = make(map[string]float64, len(gpuData))
|
||||
}
|
||||
highestTemp := 0.0
|
||||
for _, gpu := range gpuData {
|
||||
if gpu.Temperature > 0 {
|
||||
systemStats.Temperatures[gpu.Name] = gpu.Temperature
|
||||
if a.sensorConfig.primarySensor == gpu.Name {
|
||||
a.systemInfo.DashboardTemp = gpu.Temperature
|
||||
}
|
||||
if gpu.Temperature > highestTemp {
|
||||
highestTemp = gpu.Temperature
|
||||
}
|
||||
}
|
||||
// update high gpu percent for dashboard
|
||||
a.systemInfo.GpuPct = max(a.systemInfo.GpuPct, gpu.Usage)
|
||||
}
|
||||
// use highest temp for dashboard temp if dashboard temp is unset
|
||||
if a.systemInfo.DashboardTemp == 0 {
|
||||
a.systemInfo.DashboardTemp = highestTemp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +223,60 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
return systemStats
|
||||
}
|
||||
|
||||
func (a *Agent) updateTemperatures(systemStats *system.Stats) error {
|
||||
// skip if sensors whitelist is set to empty string
|
||||
if a.sensorsWhitelist != nil && len(a.sensorsWhitelist) == 0 {
|
||||
slog.Debug("Skipping temperature collection")
|
||||
return nil
|
||||
}
|
||||
|
||||
primarySensor, primarySensorIsDefined := GetEnv("PRIMARY_SENSOR")
|
||||
|
||||
// reset high temp
|
||||
a.systemInfo.DashboardTemp = 0
|
||||
|
||||
// get sensor data
|
||||
temps, err := sensors.TemperaturesWithContext(a.sensorsContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Debug("Temperature", "sensors", temps)
|
||||
|
||||
// return if no sensors
|
||||
if len(temps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
systemStats.Temperatures = make(map[string]float64, len(temps))
|
||||
for i, sensor := range temps {
|
||||
// skip if temperature is unreasonable
|
||||
if sensor.Temperature <= 0 || sensor.Temperature >= 200 {
|
||||
continue
|
||||
}
|
||||
sensorName := sensor.SensorKey
|
||||
if _, ok := systemStats.Temperatures[sensorName]; ok {
|
||||
// if key already exists, append int to key
|
||||
sensorName = sensorName + "_" + strconv.Itoa(i)
|
||||
}
|
||||
// skip if not in whitelist
|
||||
if a.sensorsWhitelist != nil {
|
||||
if _, nameInWhitelist := a.sensorsWhitelist[sensorName]; !nameInWhitelist {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// set dashboard temperature
|
||||
if primarySensorIsDefined {
|
||||
if sensorName == primarySensor {
|
||||
a.systemInfo.DashboardTemp = sensor.Temperature
|
||||
}
|
||||
} else {
|
||||
a.systemInfo.DashboardTemp = max(a.systemInfo.DashboardTemp, sensor.Temperature)
|
||||
}
|
||||
systemStats.Temperatures[sensorName] = twoDecimals(sensor.Temperature)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the size of the ZFS ARC memory cache in bytes
|
||||
func getARCSize() (uint64, error) {
|
||||
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr"
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -66,7 +66,6 @@ var supportsTitle = map[string]struct{}{
|
||||
"gotify": {},
|
||||
"ifttt": {},
|
||||
"join": {},
|
||||
"lark": {},
|
||||
"matrix": {},
|
||||
"ntfy": {},
|
||||
"opsgenie": {},
|
||||
@@ -167,12 +166,10 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
|
||||
|
||||
// Add link
|
||||
if scheme == "ntfy" {
|
||||
// if ntfy, add link to actions
|
||||
queryParams.Add("Actions", fmt.Sprintf("view, %s, %s", linkText, link))
|
||||
} else if scheme == "lark" {
|
||||
queryParams.Add("link", link)
|
||||
} else if scheme == "bark" {
|
||||
queryParams.Add("url", link)
|
||||
} else {
|
||||
// else add link directly to the message
|
||||
message += "\n\n" + link
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ type alertInfo struct {
|
||||
// startWorker is a long-running goroutine that processes alert tasks
|
||||
// every x seconds. It must be running to process status alerts.
|
||||
func (am *AlertManager) startWorker() {
|
||||
tick := time.Tick(15 * time.Second)
|
||||
// no special reason for 13 seconds
|
||||
tick := time.Tick(13 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-am.stopChan:
|
||||
@@ -63,12 +64,21 @@ func (am *AlertManager) StopWorker() {
|
||||
}
|
||||
|
||||
// HandleStatusAlerts manages the logic when system status changes.
|
||||
func (am *AlertManager) HandleStatusAlerts(newStatus string, systemRecord *core.Record) error {
|
||||
if newStatus != "up" && newStatus != "down" {
|
||||
func (am *AlertManager) HandleStatusAlerts(newStatus string, oldSystemRecord *core.Record) error {
|
||||
switch newStatus {
|
||||
case "up":
|
||||
if oldSystemRecord.GetString("status") != "down" {
|
||||
return nil
|
||||
}
|
||||
case "down":
|
||||
if oldSystemRecord.GetString("status") != "up" {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
alertRecords, err := am.getSystemStatusAlerts(systemRecord.Id)
|
||||
alertRecords, err := am.getSystemStatusAlerts(oldSystemRecord.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,7 +86,7 @@ func (am *AlertManager) HandleStatusAlerts(newStatus string, systemRecord *core.
|
||||
return nil
|
||||
}
|
||||
|
||||
systemName := systemRecord.GetString("name")
|
||||
systemName := oldSystemRecord.GetString("name")
|
||||
if newStatus == "down" {
|
||||
am.handleSystemDown(systemName, alertRecords)
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"beszel/internal/entities/system"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error {
|
||||
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, systemInfo system.Info, temperatures map[string]float64, extraFs map[string]*system.FsStats) error {
|
||||
alertRecords, err := am.app.FindAllRecords("alerts",
|
||||
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
|
||||
)
|
||||
@@ -34,15 +35,15 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
|
||||
switch name {
|
||||
case "CPU":
|
||||
val = data.Info.Cpu
|
||||
val = systemInfo.Cpu
|
||||
case "Memory":
|
||||
val = data.Info.MemPct
|
||||
val = systemInfo.MemPct
|
||||
case "Bandwidth":
|
||||
val = data.Info.Bandwidth
|
||||
val = systemInfo.Bandwidth
|
||||
unit = " MB/s"
|
||||
case "Disk":
|
||||
maxUsedPct := data.Info.DiskPct
|
||||
for _, fs := range data.Stats.ExtraFs {
|
||||
maxUsedPct := systemInfo.DiskPct
|
||||
for _, fs := range extraFs {
|
||||
usedPct := fs.DiskUsed / fs.DiskTotal * 100
|
||||
if usedPct > maxUsedPct {
|
||||
maxUsedPct = usedPct
|
||||
@@ -50,10 +51,14 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
}
|
||||
val = maxUsedPct
|
||||
case "Temperature":
|
||||
if data.Info.DashboardTemp < 1 {
|
||||
if temperatures == nil {
|
||||
continue
|
||||
}
|
||||
val = data.Info.DashboardTemp
|
||||
for _, temp := range temperatures {
|
||||
if temp > val {
|
||||
val = temp
|
||||
}
|
||||
}
|
||||
unit = "°C"
|
||||
}
|
||||
|
||||
@@ -69,8 +74,13 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
}
|
||||
|
||||
min := max(1, cast.ToUint8(alertRecord.Get("min")))
|
||||
// add time to alert time to make sure it's slighty after record creation
|
||||
time := now.Add(-time.Duration(min) * time.Minute)
|
||||
if time.Before(oldestTime) {
|
||||
oldestTime = time
|
||||
}
|
||||
|
||||
alert := SystemAlertData{
|
||||
validAlerts = append(validAlerts, SystemAlertData{
|
||||
systemRecord: systemRecord,
|
||||
alertRecord: alertRecord,
|
||||
name: name,
|
||||
@@ -78,22 +88,9 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
val: val,
|
||||
threshold: threshold,
|
||||
triggered: triggered,
|
||||
time: time,
|
||||
min: min,
|
||||
}
|
||||
|
||||
// send alert immediately if min is 1 - no need to sum up values.
|
||||
if min == 1 {
|
||||
alert.triggered = val > threshold
|
||||
go am.sendSystemAlert(alert)
|
||||
continue
|
||||
}
|
||||
|
||||
alert.time = now.Add(-time.Duration(min) * time.Minute)
|
||||
if alert.time.Before(oldestTime) {
|
||||
oldestTime = alert.time
|
||||
}
|
||||
|
||||
validAlerts = append(validAlerts, alert)
|
||||
})
|
||||
}
|
||||
|
||||
systemStats := []struct {
|
||||
@@ -114,7 +111,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
)).
|
||||
OrderBy("created").
|
||||
All(&systemStats)
|
||||
if err != nil || len(systemStats) == 0 {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,14 +119,13 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
oldestRecordTime := systemStats[0].Created.Time()
|
||||
// log.Println("oldestRecordTime", oldestRecordTime.String())
|
||||
|
||||
// Filter validAlerts to keep only those with time newer than oldestRecord
|
||||
filteredAlerts := make([]SystemAlertData, 0, len(validAlerts))
|
||||
for _, alert := range validAlerts {
|
||||
if alert.time.After(oldestRecordTime) {
|
||||
filteredAlerts = append(filteredAlerts, alert)
|
||||
// delete from validAlerts if time is older than oldestRecord
|
||||
for i := range validAlerts {
|
||||
if validAlerts[i].time.Before(oldestRecordTime) {
|
||||
// log.Println("deleting alert - time is older than oldestRecord", validAlerts[i].name, oldestRecordTime, validAlerts[i].time)
|
||||
validAlerts = slices.Delete(validAlerts, i, i+1)
|
||||
}
|
||||
}
|
||||
validAlerts = filteredAlerts
|
||||
|
||||
if len(validAlerts) == 0 {
|
||||
// log.Println("no valid alerts found")
|
||||
@@ -167,7 +163,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
alert.val += stats.NetSent + stats.NetRecv
|
||||
case "Disk":
|
||||
if alert.mapSums == nil {
|
||||
alert.mapSums = make(map[string]float32, len(data.Stats.ExtraFs)+1)
|
||||
alert.mapSums = make(map[string]float32, len(extraFs)+1)
|
||||
}
|
||||
// add root disk
|
||||
if _, ok := alert.mapSums["root"]; !ok {
|
||||
@@ -175,7 +171,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
}
|
||||
alert.mapSums["root"] += float32(stats.Disk)
|
||||
// add extra disks
|
||||
for key, fs := range data.Stats.ExtraFs {
|
||||
for key, fs := range extraFs {
|
||||
if _, ok := alert.mapSums[key]; !ok {
|
||||
alert.mapSums[key] = 0.0
|
||||
}
|
||||
|
||||
@@ -27,41 +27,38 @@ type ApiInfo struct {
|
||||
|
||||
// Docker container resources from /containers/{id}/stats
|
||||
type ApiStats struct {
|
||||
Read time.Time `json:"read"` // Time of stats generation
|
||||
NumProcs uint32 `json:"num_procs,omitzero"` // Windows specific, not populated on Linux.
|
||||
Networks map[string]NetworkStats
|
||||
CPUStats CPUStats `json:"cpu_stats"`
|
||||
MemoryStats MemoryStats `json:"memory_stats"`
|
||||
}
|
||||
// Common stats
|
||||
// Read time.Time `json:"read"`
|
||||
// PreRead time.Time `json:"preread"`
|
||||
|
||||
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuUsage [2]uint64) float64 {
|
||||
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuUsage[0]
|
||||
systemDelta := s.CPUStats.SystemUsage - prevCpuUsage[1]
|
||||
return float64(cpuDelta) / float64(systemDelta) * 100
|
||||
}
|
||||
// Linux specific stats, not populated on Windows.
|
||||
// PidsStats PidsStats `json:"pids_stats,omitempty"`
|
||||
// BlkioStats BlkioStats `json:"blkio_stats,omitempty"`
|
||||
|
||||
// from: https://github.com/docker/cli/blob/master/cli/command/container/stats_helpers.go#L185
|
||||
func (s *ApiStats) CalculateCpuPercentWindows(prevCpuUsage uint64, prevRead time.Time) float64 {
|
||||
// Max number of 100ns intervals between the previous time read and now
|
||||
possIntervals := uint64(s.Read.Sub(prevRead).Nanoseconds())
|
||||
possIntervals /= 100 // Convert to number of 100ns intervals
|
||||
possIntervals *= uint64(s.NumProcs) // Multiple by the number of processors
|
||||
// Windows specific stats, not populated on Linux.
|
||||
// NumProcs uint32 `json:"num_procs"`
|
||||
// StorageStats StorageStats `json:"storage_stats,omitempty"`
|
||||
// Networks request version >=1.21
|
||||
Networks map[string]NetworkStats
|
||||
|
||||
// Intervals used
|
||||
intervalsUsed := s.CPUStats.CPUUsage.TotalUsage - prevCpuUsage
|
||||
|
||||
// Percentage avoiding divide-by-zero
|
||||
if possIntervals > 0 {
|
||||
return float64(intervalsUsed) / float64(possIntervals) * 100.0
|
||||
}
|
||||
return 0.00
|
||||
// Shared stats
|
||||
CPUStats CPUStats `json:"cpu_stats,omitempty"`
|
||||
// PreCPUStats CPUStats `json:"precpu_stats,omitempty"` // "Pre"="Previous"
|
||||
MemoryStats MemoryStats `json:"memory_stats,omitempty"`
|
||||
}
|
||||
|
||||
type CPUStats struct {
|
||||
// CPU Usage. Linux and Windows.
|
||||
CPUUsage CPUUsage `json:"cpu_usage"`
|
||||
|
||||
// System Usage. Linux only.
|
||||
SystemUsage uint64 `json:"system_cpu_usage,omitempty"`
|
||||
|
||||
// Online CPUs. Linux only.
|
||||
// OnlineCPUs uint32 `json:"online_cpus,omitempty"`
|
||||
|
||||
// Throttling Data. Linux only.
|
||||
// ThrottlingData ThrottlingData `json:"throttling_data,omitempty"`
|
||||
}
|
||||
|
||||
type CPUUsage struct {
|
||||
@@ -69,15 +66,42 @@ type CPUUsage struct {
|
||||
// Units: nanoseconds (Linux)
|
||||
// Units: 100's of nanoseconds (Windows)
|
||||
TotalUsage uint64 `json:"total_usage"`
|
||||
|
||||
// Total CPU time consumed per core (Linux). Not used on Windows.
|
||||
// Units: nanoseconds.
|
||||
// PercpuUsage []uint64 `json:"percpu_usage,omitempty"`
|
||||
|
||||
// Time spent by tasks of the cgroup in kernel mode (Linux).
|
||||
// Time spent by all container processes in kernel mode (Windows).
|
||||
// Units: nanoseconds (Linux).
|
||||
// Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers.
|
||||
// UsageInKernelmode uint64 `json:"usage_in_kernelmode"`
|
||||
|
||||
// Time spent by tasks of the cgroup in user mode (Linux).
|
||||
// Time spent by all container processes in user mode (Windows).
|
||||
// Units: nanoseconds (Linux).
|
||||
// Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers
|
||||
// UsageInUsermode uint64 `json:"usage_in_usermode"`
|
||||
}
|
||||
|
||||
type MemoryStats struct {
|
||||
// current res_counter usage for memory
|
||||
Usage uint64 `json:"usage,omitempty"`
|
||||
// all the stats exported via memory.stat.
|
||||
Stats MemoryStatsStats `json:"stats"`
|
||||
// private working set (Windows only)
|
||||
PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"`
|
||||
Stats MemoryStatsStats `json:"stats,omitempty"`
|
||||
// maximum usage ever recorded.
|
||||
// MaxUsage uint64 `json:"max_usage,omitempty"`
|
||||
// TODO(vishh): Export these as stronger types.
|
||||
// number of times memory usage hits limits.
|
||||
// Failcnt uint64 `json:"failcnt,omitempty"`
|
||||
// Limit uint64 `json:"limit,omitempty"`
|
||||
|
||||
// // committed bytes
|
||||
// Commit uint64 `json:"commitbytes,omitempty"`
|
||||
// // peak committed bytes
|
||||
// CommitPeak uint64 `json:"commitpeakbytes,omitempty"`
|
||||
// // private working set
|
||||
// PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"`
|
||||
}
|
||||
|
||||
type MemoryStatsStats struct {
|
||||
@@ -95,6 +119,7 @@ type NetworkStats struct {
|
||||
type prevNetStats struct {
|
||||
Sent uint64
|
||||
Recv uint64
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// Docker container stats
|
||||
@@ -106,5 +131,4 @@ type Stats struct {
|
||||
NetworkRecv float64 `json:"nr"`
|
||||
PrevCpu [2]uint64 `json:"-"`
|
||||
PrevNet prevNetStats `json:"-"`
|
||||
PrevRead time.Time `json:"-"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package system
|
||||
|
||||
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"time"
|
||||
@@ -64,14 +62,6 @@ type NetIoStats struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Os uint8
|
||||
|
||||
const (
|
||||
Linux Os = iota
|
||||
Darwin
|
||||
Windows
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Hostname string `json:"h"`
|
||||
KernelVersion string `json:"k,omitempty"`
|
||||
@@ -87,7 +77,6 @@ type Info struct {
|
||||
Podman bool `json:"p,omitempty"`
|
||||
GpuPct float64 `json:"g,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty"`
|
||||
Os Os `json:"os"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
@@ -21,13 +22,12 @@ type Config struct {
|
||||
type SystemConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port,omitempty"`
|
||||
Port uint16 `yaml:"port"`
|
||||
Users []string `yaml:"users"`
|
||||
}
|
||||
|
||||
// Syncs systems with the config.yml file
|
||||
func syncSystemsWithConfig(e *core.ServeEvent) error {
|
||||
h := e.App
|
||||
func (h *Hub) syncSystemsWithConfig() error {
|
||||
configPath := filepath.Join(h.DataDir(), "config.yml")
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
@@ -89,16 +89,16 @@ func syncSystemsWithConfig(e *core.ServeEvent) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a map of existing systems
|
||||
// Create a map of existing systems for easy lookup
|
||||
existingSystemsMap := make(map[string]*core.Record)
|
||||
for _, system := range existingSystems {
|
||||
key := system.GetString("name") + system.GetString("host") + system.GetString("port")
|
||||
key := system.GetString("host") + ":" + system.GetString("port")
|
||||
existingSystemsMap[key] = system
|
||||
}
|
||||
|
||||
// Process systems from config
|
||||
for _, sysConfig := range config.Systems {
|
||||
key := sysConfig.Name + sysConfig.Host + cast.ToString(sysConfig.Port)
|
||||
key := sysConfig.Host + ":" + strconv.Itoa(int(sysConfig.Port))
|
||||
if existingSystem, ok := existingSystemsMap[key]; ok {
|
||||
// Update existing system
|
||||
existingSystem.Set("name", sysConfig.Name)
|
||||
|
||||
@@ -4,46 +4,67 @@ package hub
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/alerts"
|
||||
"beszel/internal/hub/systems"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/users"
|
||||
"beszel/site"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Hub struct {
|
||||
core.App
|
||||
*alerts.AlertManager
|
||||
um *users.UserManager
|
||||
rm *records.RecordManager
|
||||
sm *systems.SystemManager
|
||||
pubKey string
|
||||
appURL string
|
||||
*pocketbase.PocketBase
|
||||
sshClientConfig *ssh.ClientConfig
|
||||
pubKey string
|
||||
am *alerts.AlertManager
|
||||
um *users.UserManager
|
||||
rm *records.RecordManager
|
||||
systemStats *core.Collection
|
||||
containerStats *core.Collection
|
||||
appURL string
|
||||
}
|
||||
|
||||
// NewHub creates a new Hub instance with default configuration
|
||||
func NewHub(app core.App) *Hub {
|
||||
hub := &Hub{}
|
||||
hub.App = app
|
||||
func NewHub() *Hub {
|
||||
var hub Hub
|
||||
hub.PocketBase = pocketbase.NewWithConfig(pocketbase.Config{
|
||||
DefaultDataDir: beszel.AppName + "_data",
|
||||
})
|
||||
|
||||
hub.AlertManager = alerts.NewAlertManager(hub)
|
||||
hub.RootCmd.Version = beszel.Version
|
||||
hub.RootCmd.Use = beszel.AppName
|
||||
hub.RootCmd.Short = ""
|
||||
// add update command
|
||||
hub.RootCmd.AddCommand(&cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update " + beszel.AppName + " to the latest version",
|
||||
Run: Update,
|
||||
})
|
||||
|
||||
hub.am = alerts.NewAlertManager(hub)
|
||||
hub.um = users.NewUserManager(hub)
|
||||
hub.rm = records.NewRecordManager(hub)
|
||||
hub.sm = systems.NewSystemManager(hub)
|
||||
hub.appURL, _ = GetEnv("APP_URL")
|
||||
return hub
|
||||
return &hub
|
||||
}
|
||||
|
||||
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
|
||||
@@ -55,192 +76,444 @@ func GetEnv(key string) (value string, exists bool) {
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
func (h *Hub) StartHub() error {
|
||||
func (h *Hub) Run() {
|
||||
isDev := os.Getenv("ENV") == "dev"
|
||||
|
||||
h.App.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
// initialize settings / collections
|
||||
if err := h.initialize(e); err != nil {
|
||||
// enable auto creation of migration files when making collection changes in the Admin UI
|
||||
migratecmd.MustRegister(h, h.RootCmd, migratecmd.Config{
|
||||
// (the isDev check is to enable it only during development)
|
||||
Automigrate: isDev,
|
||||
Dir: "../../migrations",
|
||||
})
|
||||
|
||||
// initial setup
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
// create ssh client config
|
||||
err := h.createSSHClientConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// set general settings
|
||||
settings := h.Settings()
|
||||
// batch requests (for global alerts)
|
||||
settings.Batch.Enabled = true
|
||||
// set URL if BASE_URL env is set
|
||||
if h.appURL != "" {
|
||||
settings.Meta.AppURL = h.appURL
|
||||
}
|
||||
// set auth settings
|
||||
usersCollection, err := h.FindCollectionByNameOrId("users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
|
||||
disablePasswordAuth, _ := GetEnv("DISABLE_PASSWORD_AUTH")
|
||||
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
|
||||
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
|
||||
// disable oauth if no providers are configured (todo: remove this in post 0.9.0 release)
|
||||
if usersCollection.OAuth2.Enabled {
|
||||
usersCollection.OAuth2.Enabled = len(usersCollection.OAuth2.Providers) > 0
|
||||
}
|
||||
// allow oauth user creation if USER_CREATION is set
|
||||
if userCreation, _ := GetEnv("USER_CREATION"); userCreation == "true" {
|
||||
cr := "@request.context = 'oauth2'"
|
||||
usersCollection.CreateRule = &cr
|
||||
} else {
|
||||
usersCollection.CreateRule = nil
|
||||
}
|
||||
if err := h.Save(usersCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
// sync systems with config
|
||||
if err := syncSystemsWithConfig(e); err != nil {
|
||||
return err
|
||||
h.syncSystemsWithConfig()
|
||||
return se.Next()
|
||||
})
|
||||
|
||||
// serve web ui
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
switch 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)
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
// register api routes
|
||||
if err := h.registerApiRoutes(e); err != nil {
|
||||
return err
|
||||
return se.Next()
|
||||
})
|
||||
|
||||
// set up scheduled jobs / ticker for system updates
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
// 15 second ticker for system updates
|
||||
go h.startSystemUpdateTicker()
|
||||
// set up cron jobs
|
||||
// delete old records once every hour
|
||||
h.Cron().MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
||||
// create longer records every 10 minutes
|
||||
h.Cron().MustAdd("create longer records", "*/10 * * * *", func() {
|
||||
if systemStats, containerStats, err := h.getCollections(); err == nil {
|
||||
h.rm.CreateLongerRecords([]*core.Collection{systemStats, containerStats})
|
||||
}
|
||||
})
|
||||
return se.Next()
|
||||
})
|
||||
|
||||
// custom api routes
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
// returns public key
|
||||
se.Router.GET("/api/beszel/getkey", func(e *core.RequestEvent) error {
|
||||
info, _ := e.RequestInfo()
|
||||
if info.Auth == nil {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
return e.JSON(http.StatusOK, map[string]string{"key": h.pubKey, "v": beszel.Version})
|
||||
})
|
||||
// check if first time setup on login page
|
||||
se.Router.GET("/api/beszel/first-run", func(e *core.RequestEvent) error {
|
||||
total, err := h.CountRecords("users")
|
||||
return e.JSON(http.StatusOK, map[string]bool{"firstRun": err == nil && total == 0})
|
||||
})
|
||||
// send test notification
|
||||
se.Router.GET("/api/beszel/send-test-notification", h.am.SendTestNotification)
|
||||
// API endpoint to get config.yml content
|
||||
se.Router.GET("/api/beszel/config-yaml", h.getYamlConfig)
|
||||
// create first user endpoint only needed if no users exist
|
||||
if totalUsers, _ := h.CountRecords("users"); totalUsers == 0 {
|
||||
se.Router.POST("/api/beszel/create-user", h.um.CreateFirstUser)
|
||||
}
|
||||
// register cron jobs
|
||||
if err := h.registerCronJobs(e); err != nil {
|
||||
return err
|
||||
}
|
||||
// start server
|
||||
if err := h.startServer(e); err != nil {
|
||||
return err
|
||||
}
|
||||
// start system updates
|
||||
if err := h.sm.Initialize(); err != nil {
|
||||
return err
|
||||
return se.Next()
|
||||
})
|
||||
|
||||
// system creation defaults
|
||||
h.OnRecordCreate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
e.Record.Set("info", system.Info{})
|
||||
e.Record.Set("status", "pending")
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// immediately create connection for new systems
|
||||
h.OnRecordAfterCreateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
go h.updateSystem(e.Record)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// handle default values for user / user_settings creation
|
||||
h.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole)
|
||||
h.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings)
|
||||
|
||||
// empty info for systems that are paused
|
||||
h.OnRecordUpdate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
if e.Record.GetString("status") == "paused" {
|
||||
e.Record.Set("info", system.Info{})
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// TODO: move to users package
|
||||
// handle default values for user / user_settings creation
|
||||
h.App.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole)
|
||||
h.App.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings)
|
||||
// do things after a systems record is updated
|
||||
h.OnRecordAfterUpdateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
newRecord := e.Record.Fresh()
|
||||
oldRecord := newRecord.Original()
|
||||
newStatus := newRecord.GetString("status")
|
||||
|
||||
if pb, ok := h.App.(*pocketbase.PocketBase); ok {
|
||||
// log.Println("Starting pocketbase")
|
||||
err := pb.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
// if system is not up and connection exists, remove it
|
||||
if newStatus != "up" {
|
||||
h.deleteSystemConnection(newRecord)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// if system is set to pending (unpause), try to connect immediately
|
||||
if newStatus == "pending" {
|
||||
go h.updateSystem(newRecord)
|
||||
} else {
|
||||
h.am.HandleStatusAlerts(newStatus, oldRecord)
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// if system is deleted, close connection
|
||||
h.OnRecordAfterDeleteSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.deleteSystemConnection(e.Record)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
if err := h.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// initialize sets up initial configuration (collections, settings, etc.)
|
||||
func (h *Hub) initialize(e *core.ServeEvent) error {
|
||||
// set general settings
|
||||
settings := e.App.Settings()
|
||||
// batch requests (for global alerts)
|
||||
settings.Batch.Enabled = true
|
||||
// set URL if BASE_URL env is set
|
||||
if h.appURL != "" {
|
||||
settings.Meta.AppURL = h.appURL
|
||||
func (h *Hub) startSystemUpdateTicker() {
|
||||
c := time.Tick(15 * time.Second)
|
||||
for range c {
|
||||
h.updateSystems()
|
||||
}
|
||||
// set auth settings
|
||||
usersCollection, err := e.App.FindCollectionByNameOrId("users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Hub) updateSystems() {
|
||||
records, err := h.FindRecordsByFilter(
|
||||
"2hz5ncl8tizk5nx", // systems collection
|
||||
"status != 'paused'", // filter
|
||||
"updated", // sort
|
||||
-1, // limit
|
||||
0, // offset
|
||||
)
|
||||
// log.Println("records", len(records))
|
||||
if err != nil || len(records) == 0 {
|
||||
// h.Logger().Error("Failed to query systems")
|
||||
return
|
||||
}
|
||||
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
|
||||
disablePasswordAuth, _ := GetEnv("DISABLE_PASSWORD_AUTH")
|
||||
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
|
||||
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
|
||||
// disable oauth if no providers are configured (todo: remove this in post 0.9.0 release)
|
||||
if usersCollection.OAuth2.Enabled {
|
||||
usersCollection.OAuth2.Enabled = len(usersCollection.OAuth2.Providers) > 0
|
||||
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
|
||||
batchSize := len(records)/4 + 1
|
||||
done := 0
|
||||
for _, record := range records {
|
||||
// break if batch size reached or if the system was updated less than 50 seconds ago
|
||||
if done >= batchSize || record.GetDateTime("updated").Time().After(fiftySecondsAgo) {
|
||||
break
|
||||
}
|
||||
// don't increment for down systems to avoid them jamming the queue
|
||||
// because they're always first when sorted by least recently updated
|
||||
if record.GetString("status") != "down" {
|
||||
done++
|
||||
}
|
||||
go h.updateSystem(record)
|
||||
}
|
||||
// allow oauth user creation if USER_CREATION is set
|
||||
if userCreation, _ := GetEnv("USER_CREATION"); userCreation == "true" {
|
||||
cr := "@request.context = 'oauth2'"
|
||||
usersCollection.CreateRule = &cr
|
||||
}
|
||||
|
||||
func (h *Hub) updateSystem(record *core.Record) {
|
||||
var client *ssh.Client
|
||||
var err error
|
||||
|
||||
// check if system connection exists
|
||||
if existingClient, ok := h.Store().GetOk(record.Id); ok {
|
||||
client = existingClient.(*ssh.Client)
|
||||
} else {
|
||||
usersCollection.CreateRule = nil
|
||||
// create system connection
|
||||
client, err = h.createSystemConnection(record)
|
||||
if err != nil {
|
||||
if record.GetString("status") != "down" {
|
||||
h.Logger().Error("Failed to connect:", "err", err.Error(), "system", record.GetString("host"), "port", record.GetString("port"))
|
||||
h.updateSystemStatus(record, "down")
|
||||
}
|
||||
return
|
||||
}
|
||||
h.Store().Set(record.Id, client)
|
||||
}
|
||||
if err := e.App.Save(usersCollection); err != nil {
|
||||
// get system stats from agent
|
||||
var systemData system.CombinedData
|
||||
if err := h.requestJsonFromAgent(client, &systemData); err != nil {
|
||||
if err.Error() == "bad client" {
|
||||
// if previous connection was closed, try again
|
||||
h.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port"))
|
||||
h.deleteSystemConnection(record)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
h.updateSystem(record)
|
||||
return
|
||||
}
|
||||
h.Logger().Error("Failed to get system stats: ", "err", err.Error())
|
||||
h.updateSystemStatus(record, "down")
|
||||
return
|
||||
}
|
||||
// update system record
|
||||
record.Set("status", "up")
|
||||
record.Set("info", systemData.Info)
|
||||
if err := h.SaveNoValidate(record); err != nil {
|
||||
h.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
}
|
||||
// add system_stats and container_stats records
|
||||
if systemStats, containerStats, err := h.getCollections(); err != nil {
|
||||
h.Logger().Error("Failed to get collections: ", "err", err.Error())
|
||||
} else {
|
||||
// add new system_stats record
|
||||
systemStatsRecord := core.NewRecord(systemStats)
|
||||
systemStatsRecord.Set("system", record.Id)
|
||||
systemStatsRecord.Set("stats", systemData.Stats)
|
||||
systemStatsRecord.Set("type", "1m")
|
||||
if err := h.SaveNoValidate(systemStatsRecord); err != nil {
|
||||
h.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
}
|
||||
// add new container_stats record
|
||||
if len(systemData.Containers) > 0 {
|
||||
containerStatsRecord := core.NewRecord(containerStats)
|
||||
containerStatsRecord.Set("system", record.Id)
|
||||
containerStatsRecord.Set("stats", systemData.Containers)
|
||||
containerStatsRecord.Set("type", "1m")
|
||||
if err := h.SaveNoValidate(containerStatsRecord); err != nil {
|
||||
h.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// system info alerts
|
||||
if err := h.am.HandleSystemAlerts(record, systemData.Info, systemData.Stats.Temperatures, systemData.Stats.ExtraFs); err != nil {
|
||||
h.Logger().Error("System alerts error", "err", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// return system_stats and container_stats collections
|
||||
func (h *Hub) getCollections() (*core.Collection, *core.Collection, error) {
|
||||
if h.systemStats == nil {
|
||||
systemStats, err := h.FindCollectionByNameOrId("system_stats")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
h.systemStats = systemStats
|
||||
}
|
||||
if h.containerStats == nil {
|
||||
containerStats, err := h.FindCollectionByNameOrId("container_stats")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
h.containerStats = containerStats
|
||||
}
|
||||
return h.systemStats, h.containerStats, nil
|
||||
}
|
||||
|
||||
// set system to specified status and save record
|
||||
func (h *Hub) updateSystemStatus(record *core.Record, status string) {
|
||||
if record.Fresh().GetString("status") != status {
|
||||
record.Set("status", status)
|
||||
if err := h.SaveNoValidate(record); err != nil {
|
||||
h.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete system connection from map and close connection
|
||||
func (h *Hub) deleteSystemConnection(record *core.Record) {
|
||||
if client, ok := h.Store().GetOk(record.Id); ok {
|
||||
if sshClient := client.(*ssh.Client); sshClient != nil {
|
||||
sshClient.Close()
|
||||
}
|
||||
h.Store().Remove(record.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) createSystemConnection(record *core.Record) (*ssh.Client, error) {
|
||||
network := "tcp"
|
||||
host := record.GetString("host")
|
||||
if strings.HasPrefix(host, "/") {
|
||||
network = "unix"
|
||||
} else {
|
||||
host = net.JoinHostPort(host, record.GetString("port"))
|
||||
}
|
||||
client, err := ssh.Dial(network, host, h.sshClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (h *Hub) createSSHClientConfig() error {
|
||||
key, err := h.getSSHKey()
|
||||
if err != nil {
|
||||
h.Logger().Error("Failed to get SSH key: ", "err", err.Error())
|
||||
return err
|
||||
}
|
||||
// allow all users to access systems if SHARE_ALL_SYSTEMS is set
|
||||
systemsCollection, err := e.App.FindCachedCollectionByNameOrId("systems")
|
||||
|
||||
// Create the Signer for this private key.
|
||||
signer, err := ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shareAllSystems, _ := GetEnv("SHARE_ALL_SYSTEMS")
|
||||
systemsReadRule := "@request.auth.id != \"\""
|
||||
if shareAllSystems != "true" {
|
||||
// default is to only show systems that the user id is assigned to
|
||||
systemsReadRule += " && users.id ?= @request.auth.id"
|
||||
|
||||
h.sshClientConfig = &ssh.ClientConfig{
|
||||
User: "u",
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 4 * time.Second,
|
||||
}
|
||||
updateDeleteRule := systemsReadRule + " && @request.auth.role != \"readonly\""
|
||||
systemsCollection.ListRule = &systemsReadRule
|
||||
systemsCollection.ViewRule = &systemsReadRule
|
||||
systemsCollection.UpdateRule = &updateDeleteRule
|
||||
systemsCollection.DeleteRule = &updateDeleteRule
|
||||
if err := e.App.Save(systemsCollection); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetches system stats from the agent and decodes the json data into the provided struct
|
||||
func (h *Hub) requestJsonFromAgent(client *ssh.Client, systemData *system.CombinedData) error {
|
||||
session, err := newSessionWithTimeout(client, 4*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad client")
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// startServer starts the server for the Beszel (not PocketBase)
|
||||
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)
|
||||
// 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)
|
||||
})
|
||||
if err := session.Shell(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerCronJobs sets up scheduled tasks
|
||||
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||
// delete old records once every hour
|
||||
h.Cron().MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
||||
// create longer records every 10 minutes
|
||||
h.Cron().MustAdd("create longer records", "*/10 * * * *", h.rm.CreateLongerRecords)
|
||||
return nil
|
||||
}
|
||||
|
||||
// custom api routes
|
||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
// returns public key and version
|
||||
se.Router.GET("/api/beszel/getkey", func(e *core.RequestEvent) error {
|
||||
info, _ := e.RequestInfo()
|
||||
if info.Auth == nil {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
return e.JSON(http.StatusOK, map[string]string{"key": h.pubKey, "v": beszel.Version})
|
||||
})
|
||||
// check if first time setup on login page
|
||||
se.Router.GET("/api/beszel/first-run", func(e *core.RequestEvent) error {
|
||||
total, err := h.CountRecords("users")
|
||||
return e.JSON(http.StatusOK, map[string]bool{"firstRun": err == nil && total == 0})
|
||||
})
|
||||
// send test notification
|
||||
se.Router.GET("/api/beszel/send-test-notification", h.SendTestNotification)
|
||||
// API endpoint to get config.yml content
|
||||
se.Router.GET("/api/beszel/config-yaml", h.getYamlConfig)
|
||||
// create first user endpoint only needed if no users exist
|
||||
if totalUsers, _ := h.CountRecords("users"); totalUsers == 0 {
|
||||
se.Router.POST("/api/beszel/create-user", h.um.CreateFirstUser)
|
||||
if err := json.NewDecoder(stdout).Decode(systemData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// wait for the session to complete
|
||||
if err := session.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generates key pair if it doesn't exist and returns private key bytes
|
||||
func (h *Hub) GetSSHKey() ([]byte, error) {
|
||||
// Adds timeout to SSH session creation to avoid hanging in case of network issues
|
||||
func newSessionWithTimeout(client *ssh.Client, timeout time.Duration) (*ssh.Session, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// use goroutine to create the session
|
||||
sessionChan := make(chan *ssh.Session, 1)
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
if session, err := client.NewSession(); err != nil {
|
||||
errChan <- err
|
||||
} else {
|
||||
sessionChan <- session
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case session := <-sessionChan:
|
||||
return session, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("session creation timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) getSSHKey() ([]byte, error) {
|
||||
dataDir := h.DataDir()
|
||||
// check if the key pair already exists
|
||||
existingKey, err := os.ReadFile(dataDir + "/id_ed25519")
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
up string = "up"
|
||||
down string = "down"
|
||||
paused string = "paused"
|
||||
pending string = "pending"
|
||||
|
||||
interval int = 60_000
|
||||
|
||||
sessionTimeout = 4 * time.Second
|
||||
)
|
||||
|
||||
type SystemManager struct {
|
||||
hub hubLike
|
||||
systems *store.Store[string, *System]
|
||||
sshConfig *ssh.ClientConfig
|
||||
}
|
||||
|
||||
type System struct {
|
||||
Id string `db:"id"`
|
||||
Host string `db:"host"`
|
||||
Port string `db:"port"`
|
||||
Status string `db:"status"`
|
||||
manager *SystemManager
|
||||
client *ssh.Client
|
||||
data *system.CombinedData
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type hubLike interface {
|
||||
core.App
|
||||
GetSSHKey() ([]byte, error)
|
||||
HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error
|
||||
HandleStatusAlerts(status string, systemRecord *core.Record) error
|
||||
}
|
||||
|
||||
func NewSystemManager(hub hubLike) *SystemManager {
|
||||
return &SystemManager{
|
||||
systems: store.New(map[string]*System{}),
|
||||
hub: hub,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the system manager.
|
||||
// It binds the event hooks and starts updating existing systems.
|
||||
func (sm *SystemManager) Initialize() error {
|
||||
sm.bindEventHooks()
|
||||
// ssh setup
|
||||
key, err := sm.hub.GetSSHKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sm.createSSHClientConfig(key); err != nil {
|
||||
return err
|
||||
}
|
||||
// start updating existing systems
|
||||
var systems []*System
|
||||
err = sm.hub.DB().NewQuery("SELECT id, host, port, status FROM systems WHERE status != 'paused'").All(&systems)
|
||||
if err != nil || len(systems) == 0 {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// time between initial system updates
|
||||
delta := interval / max(1, len(systems))
|
||||
delta = min(delta, 2_000)
|
||||
sleepTime := time.Duration(delta) * time.Millisecond
|
||||
for _, system := range systems {
|
||||
time.Sleep(sleepTime)
|
||||
_ = sm.AddSystem(system)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *SystemManager) bindEventHooks() {
|
||||
sm.hub.OnRecordCreate("systems").BindFunc(sm.onRecordCreate)
|
||||
sm.hub.OnRecordAfterCreateSuccess("systems").BindFunc(sm.onRecordAfterCreateSuccess)
|
||||
sm.hub.OnRecordUpdate("systems").BindFunc(sm.onRecordUpdate)
|
||||
sm.hub.OnRecordAfterUpdateSuccess("systems").BindFunc(sm.onRecordAfterUpdateSuccess)
|
||||
sm.hub.OnRecordAfterDeleteSuccess("systems").BindFunc(sm.onRecordAfterDeleteSuccess)
|
||||
}
|
||||
|
||||
// Runs before the record is committed to the database
|
||||
func (sm *SystemManager) onRecordCreate(e *core.RecordEvent) error {
|
||||
e.Record.Set("info", system.Info{})
|
||||
e.Record.Set("status", pending)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// Runs after the record is committed to the database
|
||||
func (sm *SystemManager) onRecordAfterCreateSuccess(e *core.RecordEvent) error {
|
||||
if err := sm.AddRecord(e.Record); err != nil {
|
||||
e.App.Logger().Error("Error adding record", "err", err)
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// Runs before the record is updated
|
||||
func (sm *SystemManager) onRecordUpdate(e *core.RecordEvent) error {
|
||||
if e.Record.GetString("status") == paused {
|
||||
e.Record.Set("info", system.Info{})
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// Runs after the record is updated
|
||||
func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
|
||||
newStatus := e.Record.GetString("status")
|
||||
switch newStatus {
|
||||
case paused:
|
||||
sm.RemoveSystem(e.Record.Id)
|
||||
return e.Next()
|
||||
case pending:
|
||||
if err := sm.AddRecord(e.Record); err != nil {
|
||||
e.App.Logger().Error("Error adding record", "err", err)
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
system, ok := sm.systems.GetOk(e.Record.Id)
|
||||
if !ok {
|
||||
return sm.AddRecord(e.Record)
|
||||
}
|
||||
prevStatus := system.Status
|
||||
system.Status = newStatus
|
||||
// system alerts if system is up
|
||||
if system.Status == up {
|
||||
if err := sm.hub.HandleSystemAlerts(e.Record, system.data); err != nil {
|
||||
e.App.Logger().Error("Error handling system alerts", "err", err)
|
||||
}
|
||||
}
|
||||
if (system.Status == down && prevStatus == up) || (system.Status == up && prevStatus == down) {
|
||||
if err := sm.hub.HandleStatusAlerts(system.Status, e.Record); err != nil {
|
||||
e.App.Logger().Error("Error handling status alerts", "err", err)
|
||||
}
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// Runs after the record is deleted
|
||||
func (sm *SystemManager) onRecordAfterDeleteSuccess(e *core.RecordEvent) error {
|
||||
sm.RemoveSystem(e.Record.Id)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// AddSystem adds a system to the manager
|
||||
func (sm *SystemManager) AddSystem(sys *System) error {
|
||||
if sm.systems.Has(sys.Id) {
|
||||
return fmt.Errorf("system exists")
|
||||
}
|
||||
if sys.Id == "" || sys.Host == "" {
|
||||
return fmt.Errorf("system is missing required fields")
|
||||
}
|
||||
sys.manager = sm
|
||||
sys.ctx, sys.cancel = context.WithCancel(context.Background())
|
||||
sys.data = &system.CombinedData{}
|
||||
sm.systems.Set(sys.Id, sys)
|
||||
go sys.StartUpdater()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSystem removes a system from the manager
|
||||
func (sm *SystemManager) RemoveSystem(systemID string) error {
|
||||
system, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return fmt.Errorf("system not found")
|
||||
}
|
||||
// cancel the context to signal stop
|
||||
if system.cancel != nil {
|
||||
system.cancel()
|
||||
}
|
||||
system.resetSSHClient()
|
||||
sm.systems.Remove(systemID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRecord adds a record to the system manager.
|
||||
// It first removes any existing system with the same ID, then creates a new System
|
||||
// instance from the record data and adds it to the manager.
|
||||
// This function is typically called when a new system is created or when an existing
|
||||
// system's status changes to pending.
|
||||
func (sm *SystemManager) AddRecord(record *core.Record) (err error) {
|
||||
_ = sm.RemoveSystem(record.Id)
|
||||
system := &System{
|
||||
Id: record.Id,
|
||||
Status: record.GetString("status"),
|
||||
Host: record.GetString("host"),
|
||||
Port: record.GetString("port"),
|
||||
}
|
||||
return sm.AddSystem(system)
|
||||
}
|
||||
|
||||
// StartUpdater starts the system updater.
|
||||
// It first fetches the data from the agent then updates the records.
|
||||
// If the data is not found or the system is down, it sets the system down.
|
||||
func (sys *System) StartUpdater() {
|
||||
if sys.data == nil {
|
||||
sys.data = &system.CombinedData{}
|
||||
}
|
||||
if err := sys.update(); err != nil {
|
||||
_ = sys.setDown(err)
|
||||
}
|
||||
|
||||
c := time.Tick(time.Duration(interval) * time.Millisecond)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sys.ctx.Done():
|
||||
return
|
||||
case <-c:
|
||||
err := sys.update()
|
||||
if err != nil {
|
||||
_ = sys.setDown(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update updates the system data and records.
|
||||
// It first fetches the data from the agent then updates the records.
|
||||
func (sys *System) update() error {
|
||||
_, err := sys.fetchDataFromAgent()
|
||||
if err == nil {
|
||||
_, err = sys.createRecords()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// createRecords updates the system record and adds system_stats and container_stats records
|
||||
func (sys *System) createRecords() (*core.Record, error) {
|
||||
systemRecord, err := sys.getRecord()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hub := sys.manager.hub
|
||||
// add system_stats and container_stats records
|
||||
systemStats, err := hub.FindCachedCollectionByNameOrId("system_stats")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
systemStatsRecord := core.NewRecord(systemStats)
|
||||
systemStatsRecord.Set("system", systemRecord.Id)
|
||||
systemStatsRecord.Set("stats", sys.data.Stats)
|
||||
systemStatsRecord.Set("type", "1m")
|
||||
if err := hub.SaveNoValidate(systemStatsRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add new container_stats record
|
||||
if len(sys.data.Containers) > 0 {
|
||||
containerStats, err := hub.FindCachedCollectionByNameOrId("container_stats")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containerStatsRecord := core.NewRecord(containerStats)
|
||||
containerStatsRecord.Set("system", systemRecord.Id)
|
||||
containerStatsRecord.Set("stats", sys.data.Containers)
|
||||
containerStatsRecord.Set("type", "1m")
|
||||
if err := hub.SaveNoValidate(containerStatsRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// update system record (do this last because it triggers alerts and we need above records to be inserted first)
|
||||
systemRecord.Set("status", up)
|
||||
systemRecord.Set("info", sys.data.Info)
|
||||
if err := hub.SaveNoValidate(systemRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return systemRecord, nil
|
||||
}
|
||||
|
||||
// getRecord retrieves the system record from the database.
|
||||
// If the record is not found or the system is paused, it removes the system from the manager.
|
||||
func (sys *System) getRecord() (*core.Record, error) {
|
||||
record, err := sys.manager.hub.FindRecordById("systems", sys.Id)
|
||||
if err != nil || record == nil {
|
||||
_ = sys.manager.RemoveSystem(sys.Id)
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// setDown marks a system as down in the database.
|
||||
// It takes the original error that caused the system to go down and returns any error
|
||||
// encountered during the process of updating the system status.
|
||||
func (sys *System) setDown(OriginalError error) error {
|
||||
if sys.Status == down {
|
||||
return nil
|
||||
}
|
||||
record, err := sys.getRecord()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sys.manager.hub.Logger().Error("System down", "system", record.GetString("name"), "err", OriginalError)
|
||||
record.Set("status", down)
|
||||
err = sys.manager.hub.SaveNoValidate(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchDataFromAgent fetches the data from the agent.
|
||||
// It first creates a new SSH client if it doesn't exist or the system is down.
|
||||
// Then it creates a new SSH session and fetches the data from the agent.
|
||||
// If the data is not found or the system is down, it sets the system down.
|
||||
func (sys *System) fetchDataFromAgent() (*system.CombinedData, error) {
|
||||
maxRetries := 1
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if sys.client == nil || sys.Status == down {
|
||||
if err := sys.createSSHClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
session, err := sys.createSessionWithTimeout(4 * time.Second)
|
||||
if err != nil {
|
||||
if attempt >= maxRetries {
|
||||
return nil, err
|
||||
}
|
||||
sys.manager.hub.Logger().Warn("Session closed. Retrying...", "host", sys.Host, "port", sys.Port, "err", err)
|
||||
sys.resetSSHClient()
|
||||
continue
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := session.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this is initialized in startUpdater, should never be nil
|
||||
*sys.data = system.CombinedData{}
|
||||
if err := json.NewDecoder(stdout).Decode(sys.data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// wait for the session to complete
|
||||
if err := session.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sys.data, nil
|
||||
}
|
||||
|
||||
// this should never be reached due to the return in the loop
|
||||
return nil, fmt.Errorf("failed to fetch data")
|
||||
}
|
||||
|
||||
func (sm *SystemManager) createSSHClientConfig(key []byte) error {
|
||||
signer, err := ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sm.sshConfig = &ssh.ClientConfig{
|
||||
User: "u",
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: sessionTimeout,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSSHClient creates a new SSH client for the system
|
||||
func (s *System) createSSHClient() error {
|
||||
network := "tcp"
|
||||
host := s.Host
|
||||
if strings.HasPrefix(host, "/") {
|
||||
network = "unix"
|
||||
} else {
|
||||
host = net.JoinHostPort(host, s.Port)
|
||||
}
|
||||
var err error
|
||||
s.client, err = ssh.Dial(network, host, s.manager.sshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSessionWithTimeout creates a new SSH session with a timeout to avoid hanging
|
||||
// in case of network issues
|
||||
func (sys *System) createSessionWithTimeout(timeout time.Duration) (*ssh.Session, error) {
|
||||
if sys.client == nil {
|
||||
return nil, fmt.Errorf("client not initialized")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(sys.ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
sessionChan := make(chan *ssh.Session, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
if session, err := sys.client.NewSession(); err != nil {
|
||||
errChan <- err
|
||||
} else {
|
||||
sessionChan <- session
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case session := <-sessionChan:
|
||||
return session, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
// resetSSHClient closes the SSH connection and resets the client to nil
|
||||
func (sys *System) resetSSHClient() {
|
||||
if sys.client != nil {
|
||||
sys.client.Close()
|
||||
}
|
||||
sys.client = nil
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package systems_test
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/systems"
|
||||
"beszel/internal/tests"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createTestSystem creates a test system record with a unique host name
|
||||
// and returns the created record and any error
|
||||
func createTestSystem(t *testing.T, hub *tests.TestHub, options map[string]any) (*core.Record, error) {
|
||||
collection, err := hub.FindCachedCollectionByNameOrId("systems")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get user record
|
||||
var firstUser *core.Record
|
||||
users, err := hub.FindAllRecords("users", dbx.NewExp("id != ''"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(users) > 0 {
|
||||
firstUser = users[0]
|
||||
}
|
||||
// Generate a unique host name to ensure we're adding a new system
|
||||
uniqueHost := fmt.Sprintf("test-host-%d.example.com", time.Now().UnixNano())
|
||||
|
||||
// Create the record
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("name", uniqueHost)
|
||||
record.Set("host", uniqueHost)
|
||||
record.Set("port", "45876")
|
||||
record.Set("status", "pending")
|
||||
record.Set("users", []string{firstUser.Id})
|
||||
|
||||
// Apply any custom options
|
||||
for key, value := range options {
|
||||
record.Set(key, value)
|
||||
}
|
||||
|
||||
// Save the record to the database
|
||||
err = hub.Save(record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func TestSystemManagerIntegration(t *testing.T) {
|
||||
// Create a test hub
|
||||
hub, err := tests.NewTestHub()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer hub.Cleanup()
|
||||
|
||||
// Create independent system manager
|
||||
sm := systems.NewSystemManager(hub)
|
||||
assert.NotNil(t, sm)
|
||||
|
||||
// Test initialization
|
||||
sm.Initialize()
|
||||
|
||||
// Test collection existence. todo: move to hub package tests
|
||||
t.Run("CollectionExistence", func(t *testing.T) {
|
||||
// Verify that required collections exist
|
||||
systems, err := hub.FindCachedCollectionByNameOrId("systems")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, systems)
|
||||
|
||||
systemStats, err := hub.FindCachedCollectionByNameOrId("system_stats")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, systemStats)
|
||||
|
||||
containerStats, err := hub.FindCachedCollectionByNameOrId("container_stats")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, containerStats)
|
||||
})
|
||||
|
||||
// Test adding a system record
|
||||
t.Run("AddRecord", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// Get the count before adding the system
|
||||
countBefore := sm.GetSystemCount()
|
||||
|
||||
// record should be pending on create
|
||||
hub.OnRecordCreate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
if record.GetString("name") == "welcometoarcoampm" {
|
||||
assert.Equal(t, "pending", e.Record.GetString("status"), "System status should be 'pending'")
|
||||
wg.Done()
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// record should be down on update
|
||||
hub.OnRecordAfterUpdateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
if record.GetString("name") == "welcometoarcoampm" {
|
||||
assert.Equal(t, "down", e.Record.GetString("status"), "System status should be 'pending'")
|
||||
wg.Done()
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
// Create a test system with the first user assigned
|
||||
record, err := createTestSystem(t, hub, map[string]any{
|
||||
"name": "welcometoarcoampm",
|
||||
"host": "localhost",
|
||||
"port": "33914",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// system should be down if grabbed from the store
|
||||
assert.Equal(t, "down", sm.GetSystemStatusFromStore(record.Id), "System status should be 'down'")
|
||||
|
||||
// Check that the system count increased
|
||||
countAfter := sm.GetSystemCount()
|
||||
assert.Equal(t, countBefore+1, countAfter, "System count should increase after adding a system via event hook")
|
||||
|
||||
// Verify the system was added by checking if it exists
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should exist in the store")
|
||||
|
||||
// Verify the system host and port
|
||||
host, port := sm.GetSystemHostPort(record.Id)
|
||||
assert.Equal(t, record.Get("host"), host, "System host should match")
|
||||
assert.Equal(t, record.Get("port"), port, "System port should match")
|
||||
|
||||
// Verify the system is in the list of all system IDs
|
||||
ids := sm.GetAllSystemIDs()
|
||||
assert.Contains(t, ids, record.Id, "System ID should be in the list of all system IDs")
|
||||
|
||||
// Verify the system was added by checking if removing it works
|
||||
err = sm.RemoveSystem(record.Id)
|
||||
assert.NoError(t, err, "System should exist and be removable")
|
||||
|
||||
// Verify the system no longer exists
|
||||
assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after removal")
|
||||
|
||||
// Verify the system is not in the list of all system IDs
|
||||
newIds := sm.GetAllSystemIDs()
|
||||
assert.NotContains(t, newIds, record.Id, "System ID should not be in the list of all system IDs after removal")
|
||||
|
||||
})
|
||||
|
||||
t.Run("RemoveSystem", func(t *testing.T) {
|
||||
// Get the count before adding the system
|
||||
countBefore := sm.GetSystemCount()
|
||||
|
||||
// Create a test system record
|
||||
record, err := createTestSystem(t, hub, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the system count increased
|
||||
countAfterAdd := sm.GetSystemCount()
|
||||
assert.Equal(t, countBefore+1, countAfterAdd, "System count should increase after adding a system via event hook")
|
||||
|
||||
// Verify the system exists
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should exist in the store")
|
||||
|
||||
// Remove the system
|
||||
err = sm.RemoveSystem(record.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that the system count decreased
|
||||
countAfterRemove := sm.GetSystemCount()
|
||||
assert.Equal(t, countAfterAdd-1, countAfterRemove, "System count should decrease after removing a system")
|
||||
|
||||
// Verify the system no longer exists
|
||||
assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after removal")
|
||||
|
||||
// Verify the system is not in the list of all system IDs
|
||||
ids := sm.GetAllSystemIDs()
|
||||
assert.NotContains(t, ids, record.Id, "System ID should not be in the list of all system IDs after removal")
|
||||
|
||||
// Verify the system status is empty
|
||||
status := sm.GetSystemStatusFromStore(record.Id)
|
||||
assert.Equal(t, "", status, "System status should be empty after removal")
|
||||
|
||||
// Try to remove it again - should return an error since it's already removed
|
||||
err = sm.RemoveSystem(record.Id)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("NewRecordPending", func(t *testing.T) {
|
||||
// Create a test system
|
||||
record, err := createTestSystem(t, hub, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add the record to the system manager
|
||||
err = sm.AddRecord(record)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test filtering records by status - should be "pending" now
|
||||
filter := "status = 'pending'"
|
||||
pendingSystems, err := hub.FindRecordsByFilter("systems", filter, "-created", 0, 0, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(pendingSystems), 1)
|
||||
})
|
||||
|
||||
t.Run("SystemStatusUpdate", func(t *testing.T) {
|
||||
// Create a test system record
|
||||
record, err := createTestSystem(t, hub, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add the record to the system manager
|
||||
err = sm.AddRecord(record)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test status changes
|
||||
initialStatus := sm.GetSystemStatusFromStore(record.Id)
|
||||
|
||||
// Set a new status
|
||||
sm.SetSystemStatusInDB(record.Id, "up")
|
||||
|
||||
// Verify status was updated
|
||||
newStatus := sm.GetSystemStatusFromStore(record.Id)
|
||||
assert.Equal(t, "up", newStatus, "System status should be updated to 'up'")
|
||||
assert.NotEqual(t, initialStatus, newStatus, "Status should have changed")
|
||||
|
||||
// Verify the database was updated
|
||||
updatedRecord, err := hub.FindRecordById("systems", record.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "up", updatedRecord.Get("status"), "Database status should match")
|
||||
})
|
||||
|
||||
t.Run("HandleSystemData", func(t *testing.T) {
|
||||
// Create a test system record
|
||||
record, err := createTestSystem(t, hub, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create test system data
|
||||
testData := &system.CombinedData{
|
||||
Info: system.Info{
|
||||
Hostname: "data-test.example.com",
|
||||
KernelVersion: "5.15.0-generic",
|
||||
Cores: 4,
|
||||
Threads: 8,
|
||||
CpuModel: "Test CPU",
|
||||
Uptime: 3600,
|
||||
Cpu: 25.5,
|
||||
MemPct: 40.2,
|
||||
DiskPct: 60.0,
|
||||
Bandwidth: 100.0,
|
||||
AgentVersion: "1.0.0",
|
||||
},
|
||||
Stats: system.Stats{
|
||||
Cpu: 25.5,
|
||||
Mem: 16384.0,
|
||||
MemUsed: 6553.6,
|
||||
MemPct: 40.0,
|
||||
DiskTotal: 1024000.0,
|
||||
DiskUsed: 614400.0,
|
||||
DiskPct: 60.0,
|
||||
NetworkSent: 1024.0,
|
||||
NetworkRecv: 2048.0,
|
||||
},
|
||||
Containers: []*container.Stats{},
|
||||
}
|
||||
|
||||
// Test handling system data. todo: move to hub/alerts package tests
|
||||
err = hub.HandleSystemAlerts(record, testData)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("ErrorHandling", func(t *testing.T) {
|
||||
// Try to add a non-existent record
|
||||
nonExistentId := "non_existent_id"
|
||||
err := sm.RemoveSystem(nonExistentId)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Try to add a system with invalid host
|
||||
system := &systems.System{
|
||||
Host: "",
|
||||
}
|
||||
err = sm.AddSystem(system)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("DeleteRecord", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
runs := 0
|
||||
|
||||
hub.OnRecordUpdate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
runs++
|
||||
record := e.Record
|
||||
if record.GetString("name") == "deadflagblues" {
|
||||
if runs == 1 {
|
||||
assert.Equal(t, "up", e.Record.GetString("status"), "System status should be 'up'")
|
||||
wg.Done()
|
||||
} else if runs == 2 {
|
||||
assert.Equal(t, "paused", e.Record.GetString("status"), "System status should be 'paused'")
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// Create a test system record
|
||||
record, err := createTestSystem(t, hub, map[string]any{
|
||||
"name": "deadflagblues",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the system exists
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should exist in the store")
|
||||
|
||||
// set the status manually to up
|
||||
sm.SetSystemStatusInDB(record.Id, "up")
|
||||
|
||||
// verify the status is up
|
||||
assert.Equal(t, "up", sm.GetSystemStatusFromStore(record.Id), "System status should be 'up'")
|
||||
|
||||
// Set the status to "paused" which should cause it to be deleted from the store
|
||||
sm.SetSystemStatusInDB(record.Id, "paused")
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify the system no longer exists
|
||||
assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after deletion")
|
||||
})
|
||||
|
||||
t.Run("ConcurrentOperations", func(t *testing.T) {
|
||||
// Create a test system
|
||||
record, err := createTestSystem(t, hub, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run concurrent operations
|
||||
const goroutines = 5
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
|
||||
for i := range goroutines {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
// Alternate between different operations
|
||||
switch i % 3 {
|
||||
case 0:
|
||||
status := fmt.Sprintf("status-%d", i)
|
||||
sm.SetSystemStatusInDB(record.Id, status)
|
||||
case 1:
|
||||
_ = sm.GetSystemStatusFromStore(record.Id)
|
||||
case 2:
|
||||
_, _ = sm.GetSystemHostPort(record.Id)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify system still exists and is in a valid state
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should still exist after concurrent operations")
|
||||
status := sm.GetSystemStatusFromStore(record.Id)
|
||||
assert.NotEmpty(t, status, "System should have a status after concurrent operations")
|
||||
})
|
||||
|
||||
t.Run("ContextCancellation", func(t *testing.T) {
|
||||
// Create a test system record
|
||||
record, err := createTestSystem(t, hub, map[string]any{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the system exists in the store
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should exist in the store")
|
||||
|
||||
// Store the original context and cancel function
|
||||
originalCtx, originalCancel, err := sm.GetSystemContextFromStore(record.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure the context is not nil
|
||||
assert.NotNil(t, originalCtx, "System context should not be nil")
|
||||
assert.NotNil(t, originalCancel, "System cancel function should not be nil")
|
||||
|
||||
// Cancel the context
|
||||
originalCancel()
|
||||
|
||||
// Wait a short time for cancellation to propagate
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Verify the context is done
|
||||
select {
|
||||
case <-originalCtx.Done():
|
||||
// Context was properly cancelled
|
||||
default:
|
||||
t.Fatal("Context was not cancelled")
|
||||
}
|
||||
|
||||
// Verify the system is still in the store (cancellation shouldn't remove it)
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should still exist after context cancellation")
|
||||
|
||||
// Explicitly remove the system
|
||||
err = sm.RemoveSystem(record.Id)
|
||||
assert.NoError(t, err, "RemoveSystem should succeed")
|
||||
|
||||
// Verify the system is removed
|
||||
assert.False(t, sm.HasSystem(record.Id), "System should be removed after RemoveSystem")
|
||||
|
||||
// Try to remove it again - should return an error
|
||||
err = sm.RemoveSystem(record.Id)
|
||||
assert.Error(t, err, "RemoveSystem should fail for non-existent system")
|
||||
|
||||
// Add the system back
|
||||
err = sm.AddRecord(record)
|
||||
require.NoError(t, err, "AddRecord should succeed")
|
||||
|
||||
// Verify the system is back in the store
|
||||
assert.True(t, sm.HasSystem(record.Id), "System should exist after re-adding")
|
||||
|
||||
// Verify a new context was created
|
||||
newCtx, newCancel, err := sm.GetSystemContextFromStore(record.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, newCtx, "New system context should not be nil")
|
||||
assert.NotNil(t, newCancel, "New system cancel function should not be nil")
|
||||
assert.NotEqual(t, originalCtx, newCtx, "New context should be different from original")
|
||||
|
||||
// Clean up
|
||||
err = sm.RemoveSystem(record.Id)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package systems
|
||||
|
||||
import (
|
||||
entities "beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GetSystemCount returns the number of systems in the store
|
||||
func (sm *SystemManager) GetSystemCount() int {
|
||||
return sm.systems.Length()
|
||||
}
|
||||
|
||||
// HasSystem checks if a system with the given ID exists in the store
|
||||
func (sm *SystemManager) HasSystem(systemID string) bool {
|
||||
return sm.systems.Has(systemID)
|
||||
}
|
||||
|
||||
// GetSystemStatusFromStore returns the status of a system with the given ID
|
||||
// Returns an empty string if the system doesn't exist
|
||||
func (sm *SystemManager) GetSystemStatusFromStore(systemID string) string {
|
||||
sys, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return sys.Status
|
||||
}
|
||||
|
||||
// GetSystemContextFromStore returns the context and cancel function for a system
|
||||
func (sm *SystemManager) GetSystemContextFromStore(systemID string) (context.Context, context.CancelFunc, error) {
|
||||
sys, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("no system")
|
||||
}
|
||||
return sys.ctx, sys.cancel, nil
|
||||
}
|
||||
|
||||
// GetSystemFromStore returns a store from the system
|
||||
func (sm *SystemManager) GetSystemFromStore(systemID string) (*System, error) {
|
||||
sys, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no system")
|
||||
}
|
||||
return sys, nil
|
||||
}
|
||||
|
||||
// GetAllSystemIDs returns a slice of all system IDs in the store
|
||||
func (sm *SystemManager) GetAllSystemIDs() []string {
|
||||
data := sm.systems.GetAll()
|
||||
ids := make([]string, 0, len(data))
|
||||
for id := range data {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// GetSystemData returns the combined data for a system with the given ID
|
||||
// Returns nil if the system doesn't exist
|
||||
// This method is intended for testing
|
||||
func (sm *SystemManager) GetSystemData(systemID string) *entities.CombinedData {
|
||||
sys, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return sys.data
|
||||
}
|
||||
|
||||
// GetSystemHostPort returns the host and port for a system with the given ID
|
||||
// Returns empty strings if the system doesn't exist
|
||||
func (sm *SystemManager) GetSystemHostPort(systemID string) (string, string) {
|
||||
sys, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return "", ""
|
||||
}
|
||||
return sys.Host, sys.Port
|
||||
}
|
||||
|
||||
// DisableAutoUpdater disables the automatic updater for a system
|
||||
// This is intended for testing
|
||||
// Returns false if the system doesn't exist
|
||||
// func (sm *SystemManager) DisableAutoUpdater(systemID string) bool {
|
||||
// sys, ok := sm.systems.GetOk(systemID)
|
||||
// if !ok {
|
||||
// return false
|
||||
// }
|
||||
// if sys.cancel != nil {
|
||||
// sys.cancel()
|
||||
// sys.cancel = nil
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
|
||||
// SetSystemStatusInDB sets the status of a system directly and updates the database record
|
||||
// This is intended for testing
|
||||
// Returns false if the system doesn't exist
|
||||
func (sm *SystemManager) SetSystemStatusInDB(systemID string, status string) bool {
|
||||
if !sm.HasSystem(systemID) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update the database record
|
||||
record, err := sm.hub.FindRecordById("systems", systemID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
record.Set("status", status)
|
||||
err = sm.hub.Save(record)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -4,15 +4,14 @@ package records
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
type RecordManager struct {
|
||||
@@ -26,6 +25,11 @@ type LongerRecordData struct {
|
||||
minShorterRecords int
|
||||
}
|
||||
|
||||
type RecordDeletionData struct {
|
||||
recordType string
|
||||
retention time.Duration
|
||||
}
|
||||
|
||||
type RecordStats []struct {
|
||||
Stats []byte `db:"stats"`
|
||||
}
|
||||
@@ -35,7 +39,7 @@ func NewRecordManager(app core.App) *RecordManager {
|
||||
}
|
||||
|
||||
// Create longer records by averaging shorter records
|
||||
func (rm *RecordManager) CreateLongerRecords() {
|
||||
func (rm *RecordManager) CreateLongerRecords(collections []*core.Collection) {
|
||||
// start := time.Now()
|
||||
longerRecordData := []LongerRecordData{
|
||||
{
|
||||
@@ -66,24 +70,14 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
}
|
||||
// wrap the operations in a transaction
|
||||
rm.app.RunInTransaction(func(txApp core.App) error {
|
||||
var err error
|
||||
collections := [2]*core.Collection{}
|
||||
collections[0], err = txApp.FindCachedCollectionByNameOrId("system_stats")
|
||||
activeSystems, err := txApp.FindAllRecords("systems", dbx.NewExp("status = 'up'"))
|
||||
if err != nil {
|
||||
log.Println("failed to get active systems", "err", err.Error())
|
||||
return err
|
||||
}
|
||||
collections[1], err = txApp.FindCachedCollectionByNameOrId("container_stats")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var systems []struct {
|
||||
Id string `db:"id"`
|
||||
}
|
||||
|
||||
txApp.DB().NewQuery("SELECT id FROM systems WHERE status='up'").All(&systems)
|
||||
|
||||
// loop through all active systems, time periods, and collections
|
||||
for _, system := range systems {
|
||||
for _, system := range activeSystems {
|
||||
// log.Println("processing system", system.GetString("name"))
|
||||
for i := range longerRecordData {
|
||||
recordData := longerRecordData[i]
|
||||
@@ -98,7 +92,7 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
if recordData.longerType != "10m" {
|
||||
lastLongerRecord, err := txApp.FindFirstRecordByFilter(
|
||||
collection.Id,
|
||||
"system = {:system} && type = {:type} && created > {:created}",
|
||||
"type = {:type} && system = {:system} && created > {:created}",
|
||||
dbx.Params{"type": recordData.longerType, "system": system.Id, "created": longerRecordPeriod},
|
||||
)
|
||||
// continue if longer record exists
|
||||
@@ -114,7 +108,7 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
Select("stats").
|
||||
From(collection.Name).
|
||||
AndWhere(dbx.NewExp(
|
||||
"system={:system} AND type={:type} AND created > {:created}",
|
||||
"type={:type} AND system={:system} AND created > {:created}",
|
||||
dbx.Params{
|
||||
"type": recordData.shorterType,
|
||||
"system": system.Id,
|
||||
@@ -125,6 +119,7 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
|
||||
// continue if not enough shorter records
|
||||
if err != nil || len(stats) < recordData.minShorterRecords {
|
||||
// log.Println("not enough shorter records. continue.", len(allShorterRecords), recordData.expectedShorterRecords)
|
||||
continue
|
||||
}
|
||||
// average the shorter records and create longer record
|
||||
@@ -138,7 +133,7 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
longerRecord.Set("stats", rm.AverageContainerStats(stats))
|
||||
}
|
||||
if err := txApp.SaveNoValidate(longerRecord); err != nil {
|
||||
log.Println("failed to save longer record", "err", err)
|
||||
log.Println("failed to save longer record", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,20 +146,16 @@ func (rm *RecordManager) CreateLongerRecords() {
|
||||
}
|
||||
|
||||
// Calculate the average stats of a list of system_stats records without reflect
|
||||
func (rm *RecordManager) AverageSystemStats(records RecordStats) *system.Stats {
|
||||
sum := &system.Stats{}
|
||||
func (rm *RecordManager) AverageSystemStats(records RecordStats) system.Stats {
|
||||
sum := system.Stats{}
|
||||
count := float64(len(records))
|
||||
// use different counter for temps in case some records don't have them
|
||||
tempCount := float64(0)
|
||||
|
||||
// Temporary struct for unmarshaling
|
||||
stats := &system.Stats{}
|
||||
|
||||
// Accumulate totals
|
||||
var stats system.Stats
|
||||
for i := range records {
|
||||
*stats = system.Stats{} // Reset tempStats for unmarshaling
|
||||
if err := json.Unmarshal(records[i].Stats, stats); err != nil {
|
||||
continue
|
||||
}
|
||||
stats = system.Stats{} // Zero the struct before unmarshalling
|
||||
json.Unmarshal(records[i].Stats, &stats)
|
||||
sum.Cpu += stats.Cpu
|
||||
sum.Mem += stats.Mem
|
||||
sum.MemUsed += stats.MemUsed
|
||||
@@ -180,25 +171,26 @@ func (rm *RecordManager) AverageSystemStats(records RecordStats) *system.Stats {
|
||||
sum.DiskWritePs += stats.DiskWritePs
|
||||
sum.NetworkSent += stats.NetworkSent
|
||||
sum.NetworkRecv += stats.NetworkRecv
|
||||
// Set peak values
|
||||
// set peak values
|
||||
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||
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)
|
||||
sum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)
|
||||
|
||||
// Accumulate temperatures
|
||||
// add temps to sum
|
||||
if stats.Temperatures != nil {
|
||||
if sum.Temperatures == nil {
|
||||
sum.Temperatures = make(map[string]float64, len(stats.Temperatures))
|
||||
}
|
||||
tempCount++
|
||||
for key, value := range stats.Temperatures {
|
||||
if _, ok := sum.Temperatures[key]; !ok {
|
||||
sum.Temperatures[key] = 0
|
||||
}
|
||||
sum.Temperatures[key] += value
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate extra filesystem stats
|
||||
// add extra fs to sum
|
||||
if stats.ExtraFs != nil {
|
||||
if sum.ExtraFs == nil {
|
||||
sum.ExtraFs = make(map[string]*system.FsStats, len(stats.ExtraFs))
|
||||
@@ -207,26 +199,25 @@ func (rm *RecordManager) AverageSystemStats(records RecordStats) *system.Stats {
|
||||
if _, ok := sum.ExtraFs[key]; !ok {
|
||||
sum.ExtraFs[key] = &system.FsStats{}
|
||||
}
|
||||
fs := sum.ExtraFs[key]
|
||||
fs.DiskTotal += value.DiskTotal
|
||||
fs.DiskUsed += value.DiskUsed
|
||||
fs.DiskWritePs += value.DiskWritePs
|
||||
fs.DiskReadPs += value.DiskReadPs
|
||||
fs.MaxDiskReadPS = max(fs.MaxDiskReadPS, value.MaxDiskReadPS, value.DiskReadPs)
|
||||
fs.MaxDiskWritePS = max(fs.MaxDiskWritePS, value.MaxDiskWritePS, value.DiskWritePs)
|
||||
sum.ExtraFs[key].DiskTotal += value.DiskTotal
|
||||
sum.ExtraFs[key].DiskUsed += value.DiskUsed
|
||||
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
|
||||
sum.ExtraFs[key].DiskReadPs += value.DiskReadPs
|
||||
// peak values
|
||||
sum.ExtraFs[key].MaxDiskReadPS = max(sum.ExtraFs[key].MaxDiskReadPS, value.MaxDiskReadPS, value.DiskReadPs)
|
||||
sum.ExtraFs[key].MaxDiskWritePS = max(sum.ExtraFs[key].MaxDiskWritePS, value.MaxDiskWritePS, value.DiskWritePs)
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate GPU data
|
||||
// add GPU data
|
||||
if stats.GPUData != nil {
|
||||
if sum.GPUData == nil {
|
||||
sum.GPUData = make(map[string]system.GPUData, len(stats.GPUData))
|
||||
}
|
||||
for id, value := range stats.GPUData {
|
||||
gpu, ok := sum.GPUData[id]
|
||||
if !ok {
|
||||
gpu = system.GPUData{Name: value.Name}
|
||||
if _, ok := sum.GPUData[id]; !ok {
|
||||
sum.GPUData[id] = system.GPUData{Name: value.Name}
|
||||
}
|
||||
gpu := sum.GPUData[id]
|
||||
gpu.Temperature += value.Temperature
|
||||
gpu.MemoryUsed += value.MemoryUsed
|
||||
gpu.MemoryTotal += value.MemoryTotal
|
||||
@@ -238,67 +229,76 @@ func (rm *RecordManager) AverageSystemStats(records RecordStats) *system.Stats {
|
||||
}
|
||||
}
|
||||
|
||||
// Compute averages in place
|
||||
if count > 0 {
|
||||
sum.Cpu = twoDecimals(sum.Cpu / count)
|
||||
sum.Mem = twoDecimals(sum.Mem / count)
|
||||
sum.MemUsed = twoDecimals(sum.MemUsed / count)
|
||||
sum.MemPct = twoDecimals(sum.MemPct / count)
|
||||
sum.MemBuffCache = twoDecimals(sum.MemBuffCache / count)
|
||||
sum.MemZfsArc = twoDecimals(sum.MemZfsArc / count)
|
||||
sum.Swap = twoDecimals(sum.Swap / count)
|
||||
sum.SwapUsed = twoDecimals(sum.SwapUsed / count)
|
||||
sum.DiskTotal = twoDecimals(sum.DiskTotal / count)
|
||||
sum.DiskUsed = twoDecimals(sum.DiskUsed / count)
|
||||
sum.DiskPct = twoDecimals(sum.DiskPct / count)
|
||||
sum.DiskReadPs = twoDecimals(sum.DiskReadPs / count)
|
||||
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
|
||||
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
|
||||
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
||||
stats = system.Stats{
|
||||
Cpu: twoDecimals(sum.Cpu / count),
|
||||
Mem: twoDecimals(sum.Mem / count),
|
||||
MemUsed: twoDecimals(sum.MemUsed / count),
|
||||
MemPct: twoDecimals(sum.MemPct / count),
|
||||
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
|
||||
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
|
||||
Swap: twoDecimals(sum.Swap / count),
|
||||
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
||||
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
||||
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
||||
DiskPct: twoDecimals(sum.DiskPct / count),
|
||||
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
||||
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
||||
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
||||
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
||||
MaxCpu: sum.MaxCpu,
|
||||
MaxDiskReadPs: sum.MaxDiskReadPs,
|
||||
MaxDiskWritePs: sum.MaxDiskWritePs,
|
||||
MaxNetworkSent: sum.MaxNetworkSent,
|
||||
MaxNetworkRecv: sum.MaxNetworkRecv,
|
||||
}
|
||||
|
||||
// Average temperatures
|
||||
if sum.Temperatures != nil && tempCount > 0 {
|
||||
for key := range sum.Temperatures {
|
||||
sum.Temperatures[key] = twoDecimals(sum.Temperatures[key] / tempCount)
|
||||
}
|
||||
if sum.Temperatures != nil {
|
||||
stats.Temperatures = make(map[string]float64, len(sum.Temperatures))
|
||||
for key, value := range sum.Temperatures {
|
||||
stats.Temperatures[key] = twoDecimals(value / tempCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Average extra filesystem stats
|
||||
if sum.ExtraFs != nil {
|
||||
for key := range sum.ExtraFs {
|
||||
fs := sum.ExtraFs[key]
|
||||
fs.DiskTotal = twoDecimals(fs.DiskTotal / count)
|
||||
fs.DiskUsed = twoDecimals(fs.DiskUsed / count)
|
||||
fs.DiskWritePs = twoDecimals(fs.DiskWritePs / count)
|
||||
fs.DiskReadPs = twoDecimals(fs.DiskReadPs / count)
|
||||
}
|
||||
}
|
||||
|
||||
// Average GPU data
|
||||
if sum.GPUData != nil {
|
||||
for id := range sum.GPUData {
|
||||
gpu := sum.GPUData[id]
|
||||
gpu.Temperature = twoDecimals(gpu.Temperature / count)
|
||||
gpu.MemoryUsed = twoDecimals(gpu.MemoryUsed / count)
|
||||
gpu.MemoryTotal = twoDecimals(gpu.MemoryTotal / count)
|
||||
gpu.Usage = twoDecimals(gpu.Usage / count)
|
||||
gpu.Power = twoDecimals(gpu.Power / count)
|
||||
gpu.Count = twoDecimals(gpu.Count / count)
|
||||
sum.GPUData[id] = gpu
|
||||
if sum.ExtraFs != nil {
|
||||
stats.ExtraFs = make(map[string]*system.FsStats, len(sum.ExtraFs))
|
||||
for key, value := range sum.ExtraFs {
|
||||
stats.ExtraFs[key] = &system.FsStats{
|
||||
DiskTotal: twoDecimals(value.DiskTotal / count),
|
||||
DiskUsed: twoDecimals(value.DiskUsed / count),
|
||||
DiskWritePs: twoDecimals(value.DiskWritePs / count),
|
||||
DiskReadPs: twoDecimals(value.DiskReadPs / count),
|
||||
MaxDiskReadPS: value.MaxDiskReadPS,
|
||||
MaxDiskWritePS: value.MaxDiskWritePS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sum
|
||||
if sum.GPUData != nil {
|
||||
stats.GPUData = make(map[string]system.GPUData, len(sum.GPUData))
|
||||
for id, value := range sum.GPUData {
|
||||
stats.GPUData[id] = system.GPUData{
|
||||
Name: value.Name,
|
||||
Temperature: twoDecimals(value.Temperature / count),
|
||||
MemoryUsed: twoDecimals(value.MemoryUsed / count),
|
||||
MemoryTotal: twoDecimals(value.MemoryTotal / count),
|
||||
Usage: twoDecimals(value.Usage / count),
|
||||
Power: twoDecimals(value.Power / count),
|
||||
Count: twoDecimals(value.Count / count),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// Calculate the average stats of a list of container_stats records
|
||||
func (rm *RecordManager) AverageContainerStats(records RecordStats) []container.Stats {
|
||||
sums := make(map[string]*container.Stats)
|
||||
count := float64(len(records))
|
||||
containerStats := make([]container.Stats, 0, 50)
|
||||
|
||||
var containerStats []container.Stats
|
||||
for i := range records {
|
||||
// reset slice
|
||||
// Reset the slice length to 0, but keep the capacity
|
||||
containerStats = containerStats[:0]
|
||||
if err := json.Unmarshal(records[i].Stats, &containerStats); err != nil {
|
||||
return []container.Stats{}
|
||||
@@ -330,45 +330,38 @@ func (rm *RecordManager) AverageContainerStats(records RecordStats) []container.
|
||||
|
||||
// Deletes records older than what is displayed in the UI
|
||||
func (rm *RecordManager) DeleteOldRecords() {
|
||||
// Define the collections to process
|
||||
collections := []string{"system_stats", "container_stats"}
|
||||
|
||||
// Define record types and their retention periods
|
||||
type RecordDeletionData struct {
|
||||
recordType string
|
||||
retention time.Duration
|
||||
}
|
||||
recordData := []RecordDeletionData{
|
||||
{recordType: "1m", retention: time.Hour}, // 1 hour
|
||||
{recordType: "10m", retention: 12 * time.Hour}, // 12 hours
|
||||
{recordType: "20m", retention: 24 * time.Hour}, // 1 day
|
||||
{recordType: "120m", retention: 7 * 24 * time.Hour}, // 7 days
|
||||
{recordType: "480m", retention: 30 * 24 * time.Hour}, // 30 days
|
||||
{
|
||||
recordType: "1m",
|
||||
retention: time.Hour,
|
||||
},
|
||||
{
|
||||
recordType: "10m",
|
||||
retention: 12 * time.Hour,
|
||||
},
|
||||
{
|
||||
recordType: "20m",
|
||||
retention: 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
recordType: "120m",
|
||||
retention: 7 * 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
recordType: "480m",
|
||||
retention: 30 * 24 * time.Hour,
|
||||
},
|
||||
}
|
||||
|
||||
// Process each collection
|
||||
for _, collection := range collections {
|
||||
// Build the WHERE clause dynamically
|
||||
var conditionParts []string
|
||||
var params dbx.Params = make(map[string]any)
|
||||
|
||||
for i, rd := range recordData {
|
||||
// Create parameterized condition for this record type
|
||||
dateParam := fmt.Sprintf("date%d", i)
|
||||
conditionParts = append(conditionParts, fmt.Sprintf("(type = '%s' AND created < {:%s})", rd.recordType, dateParam))
|
||||
params[dateParam] = time.Now().UTC().Add(-rd.retention)
|
||||
}
|
||||
|
||||
// Combine conditions with OR
|
||||
conditionStr := strings.Join(conditionParts, " OR ")
|
||||
|
||||
// Construct the full raw query
|
||||
rawQuery := fmt.Sprintf("DELETE FROM %s WHERE %s", collection, conditionStr)
|
||||
|
||||
// Execute the query with parameters
|
||||
if _, err := rm.app.DB().NewQuery(rawQuery).Bind(params).Execute(); err != nil {
|
||||
// return fmt.Errorf("failed to delete from %s: %v", collection, err)
|
||||
rm.app.Logger().Error("failed to delete", "collection", collection, "error", err)
|
||||
db := rm.app.NonconcurrentDB()
|
||||
for _, recordData := range recordData {
|
||||
for _, collectionSlug := range collections {
|
||||
formattedDate := time.Now().UTC().Add(-recordData.retention).Format(types.DefaultDateLayout)
|
||||
expr := dbx.NewExp("[[created]] < {:date} AND [[type]] = {:type}", dbx.Params{"date": formattedDate, "type": recordData.recordType})
|
||||
_, err := db.Delete(collectionSlug, expr).Execute()
|
||||
if err != nil {
|
||||
rm.app.Logger().Error("Failed to delete records", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Package tests provides helpers for testing the application.
|
||||
package tests
|
||||
|
||||
import (
|
||||
"beszel/internal/hub"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
|
||||
_ "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
// TestHub is a wrapper hub instance used for testing.
|
||||
type TestHub struct {
|
||||
core.App
|
||||
*tests.TestApp
|
||||
*hub.Hub
|
||||
}
|
||||
|
||||
// NewTestHub creates and initializes a test application instance.
|
||||
//
|
||||
// It is the caller's responsibility to call app.Cleanup() when the app is no longer needed.
|
||||
func NewTestHub(optTestDataDir ...string) (*TestHub, error) {
|
||||
var testDataDir string
|
||||
if len(optTestDataDir) > 0 {
|
||||
testDataDir = optTestDataDir[0]
|
||||
}
|
||||
|
||||
return NewTestHubWithConfig(core.BaseAppConfig{
|
||||
DataDir: testDataDir,
|
||||
EncryptionEnv: "pb_test_env",
|
||||
})
|
||||
}
|
||||
|
||||
// NewTestHubWithConfig creates and initializes a test application instance
|
||||
// from the provided config.
|
||||
//
|
||||
// If config.DataDir is not set it fallbacks to the default internal test data directory.
|
||||
//
|
||||
// config.DataDir is cloned for each new test application instance.
|
||||
//
|
||||
// It is the caller's responsibility to call app.Cleanup() when the app is no longer needed.
|
||||
func NewTestHubWithConfig(config core.BaseAppConfig) (*TestHub, error) {
|
||||
testApp, err := tests.NewTestAppWithConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hub := hub.NewHub(testApp)
|
||||
|
||||
t := &TestHub{
|
||||
App: testApp,
|
||||
TestApp: testApp,
|
||||
Hub: hub,
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
1258
beszel/migrations/1732489917_collections_snapshot.go
Normal file
1258
beszel/migrations/1732489917_collections_snapshot.go
Normal file
File diff suppressed because it is too large
Load Diff
98
beszel/migrations/1738624382_updated_users.go
Normal file
98
beszel/migrations/1738624382_updated_users.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__email_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__tokenKey_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `tokenKey` + "`" + `)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("text4166911607")
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
|
||||
"exceptDomains": null,
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
"onlyDomains": null,
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "email"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__username_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (username COLLATE NOCASE)",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__email_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__tokenKey_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `tokenKey` + "`" + `)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||
"autogeneratePattern": "users[0-9]{6}",
|
||||
"hidden": false,
|
||||
"id": "text4166911607",
|
||||
"max": 150,
|
||||
"min": 3,
|
||||
"name": "username",
|
||||
"pattern": "^[\\w][\\w\\.\\-]*$",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
|
||||
"exceptDomains": null,
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
"onlyDomains": null,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": true,
|
||||
"type": "email"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
@@ -1,676 +0,0 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
// delete duplicate alerts
|
||||
app.DB().NewQuery(`
|
||||
DELETE FROM alerts
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MAX(rowid)
|
||||
FROM alerts
|
||||
GROUP BY user, system, name
|
||||
);
|
||||
`).Execute()
|
||||
|
||||
// import collections
|
||||
jsonData := `[
|
||||
{
|
||||
"id": "elngm8x1l60zi2v",
|
||||
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"viewRule": "",
|
||||
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"name": "alerts",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "hn5ly3vi",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "2hz5ncl8tizk5nx",
|
||||
"hidden": false,
|
||||
"id": "g5sl3jdg",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "system",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "zj3ingrv",
|
||||
"maxSelect": 1,
|
||||
"name": "name",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"Status",
|
||||
"CPU",
|
||||
"Memory",
|
||||
"Disk",
|
||||
"Temperature",
|
||||
"Bandwidth"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "o2ablxvn",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "value",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "fstdehcq",
|
||||
"max": 60,
|
||||
"min": null,
|
||||
"name": "min",
|
||||
"onlyInt": true,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "6hgdf6hs",
|
||||
"name": "triggered",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_MnhEt21L5r` + "`" + ` ON ` + "`" + `alerts` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `system` + "`" + `,\n ` + "`" + `name` + "`" + `\n)"
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "juohu4jipgc13v7",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"name": "container_stats",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "2hz5ncl8tizk5nx",
|
||||
"hidden": false,
|
||||
"id": "hutcu6ps",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "system",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "r39hhnil",
|
||||
"maxSize": 2000000,
|
||||
"name": "stats",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "vo7iuj96",
|
||||
"maxSelect": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"1m",
|
||||
"10m",
|
||||
"20m",
|
||||
"120m",
|
||||
"480m"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE INDEX ` + "`" + `idx_d87OiXGZD8` + "`" + ` ON ` + "`" + `container_stats` + "`" + ` (\n ` + "`" + `system` + "`" + `,\n ` + "`" + `type` + "`" + `,\n ` + "`" + `created` + "`" + `\n)"
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "ej9oowivz8b2mht",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"name": "system_stats",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "2hz5ncl8tizk5nx",
|
||||
"hidden": false,
|
||||
"id": "h9sg148r",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "system",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "azftn0be",
|
||||
"maxSize": 2000000,
|
||||
"name": "stats",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "m1ekhli3",
|
||||
"maxSelect": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"1m",
|
||||
"10m",
|
||||
"20m",
|
||||
"120m",
|
||||
"480m"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE INDEX ` + "`" + `idx_GxIee0j` + "`" + ` ON ` + "`" + `system_stats` + "`" + ` (\n ` + "`" + `system` + "`" + `,\n ` + "`" + `type` + "`" + `,\n ` + "`" + `created` + "`" + `\n)"
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "4afacsdnlu8q8r2",
|
||||
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"viewRule": null,
|
||||
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"deleteRule": null,
|
||||
"name": "user_settings",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "d5vztyxa",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "xcx4qgqq",
|
||||
"maxSize": 2000000,
|
||||
"name": "settings",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_30Lwgf2` + "`" + ` ON ` + "`" + `user_settings` + "`" + ` (` + "`" + `user` + "`" + `)"
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "2hz5ncl8tizk5nx",
|
||||
"listRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id",
|
||||
"viewRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id",
|
||||
"createRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id && @request.auth.role != \"readonly\"",
|
||||
"updateRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id && @request.auth.role != \"readonly\"",
|
||||
"deleteRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id && @request.auth.role != \"readonly\"",
|
||||
"name": "systems",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "7xloxkwk",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "waj7seaf",
|
||||
"maxSelect": 1,
|
||||
"name": "status",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"pending"
|
||||
]
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "ve781smf",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "host",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "pij0k2jk",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "port",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "qoq64ntl",
|
||||
"maxSize": 2000000,
|
||||
"name": "info",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "jcarjnjj",
|
||||
"maxSelect": 2147483647,
|
||||
"minSelect": 0,
|
||||
"name": "users",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "_pb_users_auth_",
|
||||
"listRule": "id = @request.auth.id",
|
||||
"viewRule": "id = @request.auth.id",
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"name": "users",
|
||||
"type": "auth",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cost": 10,
|
||||
"hidden": true,
|
||||
"id": "password901924565",
|
||||
"max": 0,
|
||||
"min": 8,
|
||||
"name": "password",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "password"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "[a-zA-Z0-9_]{50}",
|
||||
"hidden": true,
|
||||
"id": "text2504183744",
|
||||
"max": 60,
|
||||
"min": 30,
|
||||
"name": "tokenKey",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"exceptDomains": null,
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
"onlyDomains": null,
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "email"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "bool1547992806",
|
||||
"name": "emailVisibility",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": true,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "bool256245529",
|
||||
"name": "verified",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": true,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "users[0-9]{6}",
|
||||
"hidden": false,
|
||||
"id": "text4166911607",
|
||||
"max": 150,
|
||||
"min": 3,
|
||||
"name": "username",
|
||||
"pattern": "^[\\w][\\w\\.\\-]*$",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "qkbp58ae",
|
||||
"maxSelect": 1,
|
||||
"name": "role",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"user",
|
||||
"admin",
|
||||
"readonly"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__username_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (username COLLATE NOCASE)",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__email_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__tokenKey_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `tokenKey` + "`" + `)"
|
||||
],
|
||||
"system": false,
|
||||
"authRule": "verified=true",
|
||||
"manageRule": null
|
||||
}
|
||||
]`
|
||||
|
||||
return app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
Binary file not shown.
@@ -6,12 +6,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="./static/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Beszel</title>
|
||||
<script>
|
||||
globalThis.BESZEL = {
|
||||
BASE_PATH: "%BASE_URL%",
|
||||
HUB_VERSION: "{{V}}"
|
||||
}
|
||||
</script>
|
||||
<script>window.BASE_PATH = "%BASE_URL%"</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from "@lingui/cli"
|
||||
import type { LinguiConfig } from "@lingui/conf"
|
||||
|
||||
export default defineConfig({
|
||||
const config: LinguiConfig = {
|
||||
locales: [
|
||||
"en",
|
||||
"ar",
|
||||
@@ -33,13 +33,12 @@ export default defineConfig({
|
||||
],
|
||||
sourceLocale: "en",
|
||||
compileNamespace: "ts",
|
||||
formatOptions: {
|
||||
lineNumbers: false,
|
||||
},
|
||||
catalogs: [
|
||||
{
|
||||
path: "<rootDir>/src/locales/{locale}/{locale}",
|
||||
include: ["src"],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
2590
beszel/site/package-lock.json
generated
2590
beszel/site/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"private": true,
|
||||
"version": "0.11.0",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,10 +12,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@henrygd/queue": "^1.0.7",
|
||||
"@henrygd/semaphore": "^0.0.2",
|
||||
"@lingui/detect-locale": "^5.2.0",
|
||||
"@lingui/macro": "^5.2.0",
|
||||
"@lingui/react": "^5.2.0",
|
||||
"@lingui/detect-locale": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@nanostores/router": "^0.11.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
@@ -32,35 +31,35 @@
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.4",
|
||||
"d3-time": "^3.1.0",
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanostores": "^0.11.4",
|
||||
"pocketbase": "^0.25.2",
|
||||
"nanostores": "^0.11.3",
|
||||
"pocketbase": "^0.25.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"valibot": "^0.42.0"
|
||||
"valibot": "^0.36.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^5.2.0",
|
||||
"@lingui/swc-plugin": "^5.5.0",
|
||||
"@lingui/vite-plugin": "^5.2.0",
|
||||
"@types/bun": "^1.2.4",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/swc-plugin": "^4.1.0",
|
||||
"@lingui/vite-plugin": "^4.14.1",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-rtl": "^0.9.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.14"
|
||||
},
|
||||
"overrides": {
|
||||
"@nanostores/router": {
|
||||
@@ -70,4 +69,4 @@
|
||||
"optionalDependencies": {
|
||||
"@esbuild/linux-arm64": "^0.21.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -16,15 +14,15 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { $publicKey, pb } from "@/lib/stores"
|
||||
import { cn, copyToClipboard, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
||||
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
|
||||
import { i18n } from "@lingui/core"
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { ChevronDownIcon, Copy, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
||||
import { ChevronDownIcon, Copy, PlusIcon } from "lucide-react"
|
||||
import { memo, useRef, useState } from "react"
|
||||
import { basePath, navigate } from "./router"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
|
||||
import { SystemRecord } from "@/types"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||
|
||||
export function AddSystemButton({ className }: { className?: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -63,33 +61,25 @@ function copyDockerCompose(port = "45876", publicKey: string) {
|
||||
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||
# - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
|
||||
environment:
|
||||
LISTEN: ${port}
|
||||
PORT: ${port}
|
||||
KEY: "${publicKey}"`)
|
||||
}
|
||||
|
||||
function copyDockerRun(port = "45876", publicKey: string) {
|
||||
copyToClipboard(
|
||||
`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -e KEY="${publicKey}" -e LISTEN=${port} henrygd/beszel-agent:latest`
|
||||
`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -e KEY="${publicKey}" -e PORT=${port} henrygd/beszel-agent:latest`
|
||||
)
|
||||
}
|
||||
|
||||
function copyLinuxCommand(port = "45876", publicKey: string, brew = false) {
|
||||
let cmd = `curl -sL https://get.beszel.dev${
|
||||
brew ? "/brew" : ""
|
||||
} -o /tmp/install-agent.sh && chmod +x /tmp/install-agent.sh && /tmp/install-agent.sh -p ${port} -k "${publicKey}"`
|
||||
// brew script does not support --china-mirrors
|
||||
if (!brew && (i18n.locale + navigator.language).includes("zh-CN")) {
|
||||
function copyInstallCommand(port = "45876", publicKey: string) {
|
||||
let cmd = `curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh && chmod +x install-agent.sh && ./install-agent.sh -p ${port} -k "${publicKey}"`
|
||||
// add china mirrors flag if zh-CN
|
||||
if ((i18n.locale + navigator.language).includes("zh-CN")) {
|
||||
cmd += ` --china-mirrors`
|
||||
}
|
||||
copyToClipboard(cmd)
|
||||
}
|
||||
|
||||
function copyWindowsCommand(port = "45876", publicKey: string) {
|
||||
copyToClipboard(
|
||||
`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser; & iwr -useb https://get.beszel.dev -OutFile "$env:TEMP\install-agent.ps1"; & "$env:TEMP\install-agent.ps1" -Key "${publicKey}" -Port ${port}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemDialog component for adding or editing a system.
|
||||
* @param {Object} props - The component props.
|
||||
@@ -101,7 +91,6 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
||||
const port = useRef<HTMLInputElement>(null)
|
||||
const [hostValue, setHostValue] = useState(system?.host ?? "")
|
||||
const isUnixSocket = hostValue.startsWith("/")
|
||||
const [tab, setTab] = useLocalStorage("as-tab", "docker")
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
@@ -129,7 +118,7 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
||||
setHostValue(system?.host ?? "")
|
||||
}}
|
||||
>
|
||||
<Tabs defaultValue={tab} onValueChange={setTab}>
|
||||
<Tabs defaultValue="docker">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-2">
|
||||
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
|
||||
@@ -151,7 +140,7 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary" tabIndex={-1}>
|
||||
<TabsContent value="binary">
|
||||
<DialogDescription className="mb-4 leading-normal w-0 min-w-full">
|
||||
<Trans>
|
||||
The agent must be running on the system to connect. Copy the installation command for the agent below.
|
||||
@@ -206,7 +195,7 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
||||
className="absolute end-0 top-0"
|
||||
onClick={() => copyToClipboard(publicKey)}
|
||||
>
|
||||
<Copy className="size-4" />
|
||||
<Copy className="h-4 w-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -222,41 +211,19 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
||||
{/* Docker */}
|
||||
<TabsContent value="docker" className="contents">
|
||||
<CopyButton
|
||||
text={t({ message: "Copy docker compose", context: "Button to copy docker compose file content" })}
|
||||
text={t`Copy` + " docker compose"}
|
||||
onClick={() => copyDockerCompose(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
icon={<DockerIcon className="size-4 -me-0.5" />}
|
||||
dropdownItems={[
|
||||
{
|
||||
text: t({ message: "Copy docker run", context: "Button to copy docker run command" }),
|
||||
onClick: () => copyDockerRun(isUnixSocket ? hostValue : port.current?.value, publicKey),
|
||||
icons: [<DockerIcon className="size-4" />],
|
||||
},
|
||||
]}
|
||||
dropdownText={t`Copy` + " docker run"}
|
||||
dropdownOnClick={() => copyDockerRun(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
/>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary" className="contents">
|
||||
<CopyButton
|
||||
text={t`Copy Linux command`}
|
||||
icon={<TuxIcon className="size-4" />}
|
||||
onClick={() => copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
dropdownItems={[
|
||||
{
|
||||
text: t({ message: "Homebrew command", context: "Button to copy install command" }),
|
||||
onClick: () => copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, true),
|
||||
icons: [<AppleIcon className="size-4" />, <TuxIcon className="w-4 h-4" />],
|
||||
},
|
||||
{
|
||||
text: t({ message: "Windows command", context: "Button to copy install command" }),
|
||||
onClick: () => copyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey),
|
||||
icons: [<WindowsIcon className="size-4" />],
|
||||
},
|
||||
{
|
||||
text: t`Manual setup instructions`,
|
||||
url: "https://beszel.dev/guide/agent-installation#binary",
|
||||
icons: [<ExternalLinkIcon className="size-4" />],
|
||||
},
|
||||
]}
|
||||
onClick={() => copyInstallCommand(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
dropdownText={t`Manual setup instructions`}
|
||||
dropdownUrl="https://beszel.dev/guide/agent-installation#binary"
|
||||
/>
|
||||
</TabsContent>
|
||||
{/* Save */}
|
||||
@@ -268,30 +235,19 @@ export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean
|
||||
)
|
||||
})
|
||||
|
||||
interface DropdownItem {
|
||||
text: string
|
||||
onClick?: () => void
|
||||
url?: string
|
||||
icons?: React.ReactNode[]
|
||||
}
|
||||
|
||||
interface CopyButtonProps {
|
||||
text: string
|
||||
onClick: () => void
|
||||
dropdownItems: DropdownItem[]
|
||||
icon?: React.ReactNode
|
||||
dropdownText: string
|
||||
dropdownOnClick?: () => void
|
||||
dropdownUrl?: string
|
||||
}
|
||||
|
||||
const CopyButton = memo((props: CopyButtonProps) => {
|
||||
return (
|
||||
<div className="flex gap-0 rounded-lg">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={props.onClick}
|
||||
className="rounded-e-none dark:border-e-0 grow flex items-center gap-2"
|
||||
>
|
||||
{props.text} {props.icon}
|
||||
<Button type="button" variant="outline" onClick={props.onClick} className="rounded-e-none dark:border-e-0 grow">
|
||||
{props.text}
|
||||
</Button>
|
||||
<div className="w-px h-full bg-muted"></div>
|
||||
<DropdownMenu>
|
||||
@@ -301,24 +257,15 @@ const CopyButton = memo((props: CopyButtonProps) => {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{props.dropdownItems.map((item, index) => (
|
||||
<DropdownMenuItem key={index} asChild={!!item.url}>
|
||||
{item.url ? (
|
||||
<a
|
||||
href={item.url}
|
||||
className="cursor-pointer flex items-center gap-1.5"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{item.text} {item.icons?.map((icon) => icon)}
|
||||
</a>
|
||||
) : (
|
||||
<div onClick={item.onClick} className="cursor-pointer flex items-center gap-1.5">
|
||||
{item.text} {item.icons?.map((icon) => icon)}
|
||||
</div>
|
||||
)}
|
||||
{props.dropdownUrl ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<a href={props.dropdownUrl} target="_blank" rel="noopener noreferrer">
|
||||
{props.dropdownText}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
) : (
|
||||
<DropdownMenuItem onClick={props.dropdownOnClick}>{props.dropdownText}</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { memo, useMemo, useState } from "react"
|
||||
import { memo, useState } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $alerts } from "@/lib/stores"
|
||||
import { $alerts, $systems } from "@/lib/stores"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -19,114 +17,104 @@ import { Link } from "../router"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Checkbox } from "../ui/checkbox"
|
||||
import { SystemAlert, SystemAlertGlobal } from "./alerts-system"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
|
||||
export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
const alerts = useStore($alerts)
|
||||
const [opened, setOpened] = useState(false)
|
||||
|
||||
const hasAlert = alerts.some((alert) => alert.system === system.id)
|
||||
const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
|
||||
const active = systemAlerts.length > 0
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
||||
<BellIcon
|
||||
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||
"fill-primary": hasAlert,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
||||
{opened && <AlertDialogContent system={system} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
[opened, hasAlert]
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
||||
<BellIcon
|
||||
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||
"fill-primary": active,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
||||
{opened && <TheContent data={{ system, alerts, systemAlerts }} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
function AlertDialogContent({ system }: { system: SystemRecord }) {
|
||||
const alerts = useStore($alerts)
|
||||
function TheContent({
|
||||
data: { system, alerts, systemAlerts },
|
||||
}: {
|
||||
data: { system: SystemRecord; alerts: AlertRecord[]; systemAlerts: AlertRecord[] }
|
||||
}) {
|
||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
||||
const systems = $systems.get()
|
||||
|
||||
// alertsSignature changes only when alerts for this system change
|
||||
let alertsSignature = ""
|
||||
const systemAlerts = alerts.filter((alert) => {
|
||||
if (alert.system === system.id) {
|
||||
alertsSignature += alert.name + alert.min + alert.value
|
||||
return true
|
||||
const data = Object.keys(alertInfo).map((key) => {
|
||||
const alert = alertInfo[key as keyof typeof alertInfo]
|
||||
return {
|
||||
key: key as keyof typeof alertInfo,
|
||||
alert,
|
||||
system,
|
||||
}
|
||||
return false
|
||||
}) as AlertRecord[]
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
// console.log("render modal", system.name, alertsSignature)
|
||||
const data = Object.keys(alertInfo).map((name) => {
|
||||
const alert = alertInfo[name as keyof typeof alertInfo]
|
||||
return {
|
||||
name: name as keyof typeof alertInfo,
|
||||
alert,
|
||||
system,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
<Trans>Alerts</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
See{" "}
|
||||
<Link href="/settings/notifications" className="link">
|
||||
notification settings
|
||||
</Link>{" "}
|
||||
to configure how you receive alerts.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="system">
|
||||
<TabsList className="mb-1 -mt-0.5">
|
||||
<TabsTrigger value="system">
|
||||
<ServerIcon className="me-2 h-3.5 w-3.5" />
|
||||
{system.name}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="global">
|
||||
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
|
||||
<Trans>All Systems</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="system">
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlert key={d.name} system={system} data={d} systemAlerts={systemAlerts} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="global">
|
||||
<label
|
||||
htmlFor="ovw"
|
||||
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
|
||||
>
|
||||
<Checkbox
|
||||
id="ovw"
|
||||
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
|
||||
checked={overwriteExisting}
|
||||
onCheckedChange={setOverwriteExisting}
|
||||
/>
|
||||
<Trans>Overwrite existing alerts</Trans>
|
||||
</label>
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlertGlobal key={d.name} data={d} overwrite={overwriteExisting} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}, [alertsSignature, overwriteExisting])
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
<Trans>Alerts</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
See{" "}
|
||||
<Link href="/settings/notifications" className="link">
|
||||
notification settings
|
||||
</Link>{" "}
|
||||
to configure how you receive alerts.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="system">
|
||||
<TabsList className="mb-1 -mt-0.5">
|
||||
<TabsTrigger value="system">
|
||||
<ServerIcon className="me-2 h-3.5 w-3.5" />
|
||||
{system.name}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="global">
|
||||
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
|
||||
<Trans>All Systems</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="system">
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlert key={d.key} system={system} data={d} systemAlerts={systemAlerts} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="global">
|
||||
<label
|
||||
htmlFor="ovw"
|
||||
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
|
||||
>
|
||||
<Checkbox
|
||||
id="ovw"
|
||||
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
|
||||
checked={overwriteExisting}
|
||||
onCheckedChange={setOverwriteExisting}
|
||||
/>
|
||||
<Trans>Overwrite existing alerts</Trans>
|
||||
</label>
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlertGlobal key={d.key} data={d} overwrite={overwriteExisting} alerts={alerts} systems={systems} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans, Plural } from "@lingui/react/macro"
|
||||
import { $alerts, $systems, pb } from "@/lib/stores"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { alertInfo, cn } from "@/lib/utils"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
||||
import { lazy, Suspense, useMemo, useState } from "react"
|
||||
import { lazy, Suspense, useRef, useState } from "react"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { BatchService } from "pocketbase"
|
||||
import { getSemaphore } from "@henrygd/semaphore"
|
||||
import { RecordOptions } from "pocketbase"
|
||||
import { Trans, t, Plural } from "@lingui/macro"
|
||||
|
||||
interface AlertData {
|
||||
checked?: boolean
|
||||
val?: number
|
||||
min?: number
|
||||
updateAlert?: (checked: boolean, value: number, min: number) => void
|
||||
name: keyof typeof alertInfo
|
||||
key: keyof typeof alertInfo
|
||||
alert: AlertInfo
|
||||
system: SystemRecord
|
||||
}
|
||||
@@ -37,7 +35,7 @@ export function SystemAlert({
|
||||
systemAlerts: AlertRecord[]
|
||||
data: AlertData
|
||||
}) {
|
||||
const alert = systemAlerts.find((alert) => alert.name === data.name)
|
||||
const alert = systemAlerts.find((alert) => alert.name === data.key)
|
||||
|
||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||
try {
|
||||
@@ -49,7 +47,7 @@ export function SystemAlert({
|
||||
pb.collection("alerts").create({
|
||||
system: system.id,
|
||||
user: pb.authStore.record!.id,
|
||||
name: data.name,
|
||||
name: data.key,
|
||||
value: value,
|
||||
min: min,
|
||||
})
|
||||
@@ -68,150 +66,99 @@ export function SystemAlert({
|
||||
return <AlertContent data={data} />
|
||||
}
|
||||
|
||||
export const SystemAlertGlobal = ({ data, overwrite }: { data: AlertData; overwrite: boolean | "indeterminate" }) => {
|
||||
export function SystemAlertGlobal({
|
||||
data,
|
||||
overwrite,
|
||||
alerts,
|
||||
systems,
|
||||
}: {
|
||||
data: AlertData
|
||||
overwrite: boolean | "indeterminate"
|
||||
alerts: AlertRecord[]
|
||||
systems: SystemRecord[]
|
||||
}) {
|
||||
const systemsWithExistingAlerts = useRef<{ set: Set<string>; populatedSet: boolean }>({
|
||||
set: new Set(),
|
||||
populatedSet: false,
|
||||
})
|
||||
|
||||
data.checked = false
|
||||
data.val = data.min = 0
|
||||
|
||||
// set of system ids that have an alert for this name when the component is mounted
|
||||
const existingAlertsSystems = useMemo(() => {
|
||||
const map = new Set<string>()
|
||||
const alerts = $alerts.get()
|
||||
for (const alert of alerts) {
|
||||
if (alert.name === data.name) {
|
||||
map.add(alert.system)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [])
|
||||
|
||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||
const sem = getSemaphore("alerts")
|
||||
await sem.acquire()
|
||||
try {
|
||||
// if another update is waiting behind, don't start this one
|
||||
if (sem.size() > 1) {
|
||||
return
|
||||
}
|
||||
const { set, populatedSet } = systemsWithExistingAlerts.current
|
||||
|
||||
const recordData: Partial<AlertRecord> = {
|
||||
value,
|
||||
min,
|
||||
triggered: false,
|
||||
}
|
||||
// if overwrite checked, make sure all alerts will be overwritten
|
||||
if (overwrite) {
|
||||
set.clear()
|
||||
}
|
||||
|
||||
const batch = batchWrapper("alerts", 25)
|
||||
const systems = $systems.get()
|
||||
const currentAlerts = $alerts.get()
|
||||
const recordData: Partial<AlertRecord> = {
|
||||
value,
|
||||
min,
|
||||
triggered: false,
|
||||
}
|
||||
|
||||
// map of current alerts with this name right now by system id
|
||||
const currentAlertsSystems = new Map<string, AlertRecord>()
|
||||
for (const alert of currentAlerts) {
|
||||
if (alert.name === data.name) {
|
||||
currentAlertsSystems.set(alert.system, alert)
|
||||
}
|
||||
}
|
||||
// we can only send 50 in one batch
|
||||
let done = 0
|
||||
|
||||
if (overwrite) {
|
||||
existingAlertsSystems.clear()
|
||||
}
|
||||
while (done < systems.length) {
|
||||
const batch = pb.createBatch()
|
||||
let batchSize = 0
|
||||
|
||||
const processSystem = async (system: SystemRecord): Promise<void> => {
|
||||
const existingAlert = existingAlertsSystems.has(system.id)
|
||||
|
||||
if (!overwrite && existingAlert) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentAlert = currentAlertsSystems.get(system.id)
|
||||
|
||||
// delete existing alert if unchecked
|
||||
if (!checked && currentAlert) {
|
||||
return batch.remove(currentAlert.id)
|
||||
}
|
||||
if (checked && currentAlert) {
|
||||
// update existing alert if checked
|
||||
return batch.update(currentAlert.id, recordData)
|
||||
}
|
||||
if (checked) {
|
||||
// create new alert if checked and not existing
|
||||
return batch.create({
|
||||
system: system.id,
|
||||
user: pb.authStore.record!.id,
|
||||
name: data.name,
|
||||
...recordData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// make sure current system is updated in the first batch
|
||||
await processSystem(data.system)
|
||||
for (const system of systems) {
|
||||
if (system.id === data.system.id) {
|
||||
for (let i = done; i < Math.min(done + 50, systems.length); i++) {
|
||||
const system = systems[i]
|
||||
// if overwrite is false and system is in set (alert existed), skip
|
||||
if (!overwrite && set.has(system.id)) {
|
||||
continue
|
||||
}
|
||||
if (sem.size() > 1) {
|
||||
return
|
||||
// find matching existing alert
|
||||
const existingAlert = alerts.find((alert) => alert.system === system.id && data.key === alert.name)
|
||||
// if first run, add system to set (alert already existed when global panel was opened)
|
||||
if (existingAlert && !populatedSet && !overwrite) {
|
||||
set.add(system.id)
|
||||
continue
|
||||
}
|
||||
batchSize++
|
||||
const requestOptions: RecordOptions = {
|
||||
requestKey: system.id,
|
||||
}
|
||||
|
||||
// checked - make sure alert is created or updated
|
||||
if (checked) {
|
||||
if (existingAlert) {
|
||||
batch.collection("alerts").update(existingAlert.id, recordData, requestOptions)
|
||||
} else {
|
||||
batch.collection("alerts").create(
|
||||
{
|
||||
system: system.id,
|
||||
user: pb.authStore.record!.id,
|
||||
name: data.key,
|
||||
...recordData,
|
||||
},
|
||||
requestOptions
|
||||
)
|
||||
}
|
||||
} else if (existingAlert) {
|
||||
batch.collection("alerts").delete(existingAlert.id)
|
||||
}
|
||||
await processSystem(system)
|
||||
}
|
||||
await batch.send()
|
||||
} finally {
|
||||
sem.release()
|
||||
try {
|
||||
batchSize && batch.send()
|
||||
} catch (e) {
|
||||
failedUpdateToast()
|
||||
} finally {
|
||||
done += 50
|
||||
}
|
||||
}
|
||||
systemsWithExistingAlerts.current.populatedSet = true
|
||||
}
|
||||
|
||||
return <AlertContent data={data} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper for performing batch operations on a specified collection.
|
||||
*/
|
||||
function batchWrapper(collection: string, batchSize: number) {
|
||||
let batch: BatchService | undefined
|
||||
let count = 0
|
||||
|
||||
const create = async <T extends Record<string, any>>(options: T) => {
|
||||
batch ||= pb.createBatch()
|
||||
batch.collection(collection).create(options)
|
||||
if (++count >= batchSize) {
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
const update = async <T extends Record<string, any>>(id: string, data: T) => {
|
||||
batch ||= pb.createBatch()
|
||||
batch.collection(collection).update(id, data)
|
||||
if (++count >= batchSize) {
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async (id: string) => {
|
||||
batch ||= pb.createBatch()
|
||||
batch.collection(collection).delete(id)
|
||||
if (++count >= batchSize) {
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
const send = async () => {
|
||||
if (count) {
|
||||
await batch?.send({ requestKey: null })
|
||||
batch = undefined
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
remove,
|
||||
send,
|
||||
create,
|
||||
}
|
||||
}
|
||||
|
||||
function AlertContent({ data }: { data: AlertData }) {
|
||||
const { name } = data
|
||||
const { key } = data
|
||||
|
||||
const singleDescription = data.alert.singleDesc?.()
|
||||
|
||||
@@ -219,12 +166,17 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
const [min, setMin] = useState(data.min || 10)
|
||||
const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80))
|
||||
|
||||
const Icon = alertInfo[name].icon
|
||||
const newMin = useRef(min)
|
||||
const newValue = useRef(value)
|
||||
|
||||
const Icon = alertInfo[key].icon
|
||||
|
||||
const updateAlert = (c?: boolean) => data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
||||
<label
|
||||
htmlFor={`s${name}`}
|
||||
htmlFor={`s${key}`}
|
||||
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
|
||||
"pb-0": checked,
|
||||
})}
|
||||
@@ -236,67 +188,56 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
{!checked && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
|
||||
</div>
|
||||
<Switch
|
||||
id={`s${name}`}
|
||||
id={`s${key}`}
|
||||
checked={checked}
|
||||
onCheckedChange={(newChecked) => {
|
||||
setChecked(newChecked)
|
||||
data.updateAlert?.(newChecked, value, min)
|
||||
onCheckedChange={(checked) => {
|
||||
setChecked(checked)
|
||||
updateAlert(checked)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{checked && (
|
||||
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
||||
<Suspense fallback={<div className="h-10" />}>
|
||||
{!singleDescription && (
|
||||
<div>
|
||||
<p id={`v${name}`} className="text-sm block h-8">
|
||||
<Trans>
|
||||
Average exceeds{" "}
|
||||
<strong className="text-foreground">
|
||||
{value}
|
||||
{data.alert.unit}
|
||||
</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Slider
|
||||
aria-labelledby={`v${name}`}
|
||||
defaultValue={[value]}
|
||||
onValueCommit={(val) => {
|
||||
data.updateAlert?.(true, val[0], min)
|
||||
}}
|
||||
onValueChange={(val) => {
|
||||
setValue(val[0])
|
||||
}}
|
||||
min={1}
|
||||
max={alertInfo[name].max ?? 99}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(singleDescription && "col-span-full lowercase")}>
|
||||
<p id={`t${name}`} className="text-sm block h-8 first-letter:uppercase">
|
||||
{singleDescription && (
|
||||
<>
|
||||
{singleDescription}
|
||||
{` `}
|
||||
</>
|
||||
)}
|
||||
{!singleDescription && (
|
||||
<div>
|
||||
<p id={`v${key}`} className="text-sm block h-8">
|
||||
<Trans>
|
||||
For <strong className="text-foreground">{min}</strong>{" "}
|
||||
<Plural value={min} one="minute" other="minutes" />
|
||||
Average exceeds{" "}
|
||||
<strong className="text-foreground">
|
||||
{value}
|
||||
{data.alert.unit}
|
||||
</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Slider
|
||||
aria-labelledby={`v${name}`}
|
||||
aria-labelledby={`v${key}`}
|
||||
defaultValue={[value]}
|
||||
onValueCommit={(val) => (newValue.current = val[0]) && updateAlert()}
|
||||
onValueChange={(val) => setValue(val[0])}
|
||||
min={1}
|
||||
max={alertInfo[key].max ?? 99}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(singleDescription && "col-span-full lowercase")}>
|
||||
<p id={`t${key}`} className="text-sm block h-8 first-letter:uppercase">
|
||||
{singleDescription && (
|
||||
<>{singleDescription}{` `}</>
|
||||
)}
|
||||
<Trans>
|
||||
For <strong className="text-foreground">{min}</strong>{" "}
|
||||
<Plural value={min} one=" minute" other=" minutes" />
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Slider
|
||||
aria-labelledby={`v${key}`}
|
||||
defaultValue={[min]}
|
||||
onValueCommit={(min) => {
|
||||
data.updateAlert?.(true, value, min[0])
|
||||
}}
|
||||
onValueChange={(val) => {
|
||||
setMin(val[0])
|
||||
}}
|
||||
onValueCommit={(val) => (newMin.current = val[0]) && updateAlert()}
|
||||
onValueChange={(val) => setMin(val[0])}
|
||||
min={1}
|
||||
max={60}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
@@ -13,7 +12,8 @@ import {
|
||||
// import Spinner from '../spinner'
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
|
||||
/** [label, key, color, opacity] */
|
||||
type DataKeys = [string, string, number, number]
|
||||
@@ -35,7 +35,6 @@ export default memo(function AreaChartDefault({
|
||||
chartData,
|
||||
max,
|
||||
tickFormatter,
|
||||
contentFormatter,
|
||||
}: {
|
||||
maxToggled?: boolean
|
||||
unit?: string
|
||||
@@ -43,7 +42,6 @@ export default memo(function AreaChartDefault({
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
tickFormatter?: (value: number) => string
|
||||
contentFormatter?: (value: number) => string
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const { i18n } = useLingui()
|
||||
@@ -117,12 +115,7 @@ export default memo(function AreaChartDefault({
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={({ value }) => {
|
||||
if (contentFormatter) {
|
||||
return contentFormatter(value)
|
||||
}
|
||||
return decimalString(value) + unit
|
||||
}}
|
||||
contentFormatter={(item) => decimalString(item.value) + unit}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -16,17 +16,16 @@ import { useStore } from "@nanostores/react"
|
||||
import { $containerFilter } from "@/lib/stores"
|
||||
import { ChartData } from "@/types"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { ChartType } from "@/lib/enums"
|
||||
|
||||
export default memo(function ContainerChart({
|
||||
dataKey,
|
||||
chartData,
|
||||
chartType,
|
||||
chartName,
|
||||
unit = "%",
|
||||
}: {
|
||||
dataKey: string
|
||||
chartData: ChartData
|
||||
chartType: ChartType
|
||||
chartName: string
|
||||
unit?: string
|
||||
}) {
|
||||
const filter = useStore($containerFilter)
|
||||
@@ -34,7 +33,7 @@ export default memo(function ContainerChart({
|
||||
|
||||
const { containerData } = chartData
|
||||
|
||||
const isNetChart = chartType === ChartType.Network
|
||||
const isNetChart = chartName === "net"
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
let config = {} as Record<
|
||||
@@ -82,7 +81,7 @@ export default memo(function ContainerChart({
|
||||
tickFormatter: (value: any) => string
|
||||
}
|
||||
// tick formatter
|
||||
if (chartType === ChartType.CPU) {
|
||||
if (chartName === "cpu") {
|
||||
obj.tickFormatter = (value) => {
|
||||
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||
return updateYAxisWidth(val)
|
||||
@@ -112,11 +111,6 @@ export default memo(function ContainerChart({
|
||||
return null
|
||||
}
|
||||
}
|
||||
} else if (chartType === ChartType.Memory) {
|
||||
obj.toolTipFormatter = (item: any) => {
|
||||
const { v, u } = getSizeAndUnit(item.value, false)
|
||||
return decimalString(v, 2) + u
|
||||
}
|
||||
} else {
|
||||
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
||||
}
|
||||
@@ -163,14 +157,13 @@ export default memo(function ContainerChart({
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
truncate={true}
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
// @ts-ignore
|
||||
itemSorter={(a, b) => b.value - a.value}
|
||||
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
||||
/>
|
||||
{Object.keys(chartConfig).map((key) => {
|
||||
const filtered = filter && !key.toLowerCase().includes(filter.toLowerCase())
|
||||
const filtered = filter && !key.includes(filter)
|
||||
let fillOpacity = filtered ? 0.05 : 0.4
|
||||
let strokeOpacity = filtered ? 0.1 : 1
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
|
||||
export default memo(function DiskChart({
|
||||
dataKey,
|
||||
@@ -23,7 +25,7 @@ export default memo(function DiskChart({
|
||||
chartData: ChartData
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const { t } = useLingui()
|
||||
const { _ } = useLingui()
|
||||
|
||||
// round to nearest GB
|
||||
if (diskSize >= 100) {
|
||||
@@ -74,7 +76,7 @@ export default memo(function DiskChart({
|
||||
/>
|
||||
<Area
|
||||
dataKey={dataKey}
|
||||
name={t`Disk Usage`}
|
||||
name={_(t`Disk Usage`)}
|
||||
type="monotoneX"
|
||||
fill="hsl(var(--chart-4))"
|
||||
fillOpacity={0.4}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
import { memo } from "react"
|
||||
import { ChartData } from "@/types"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
|
||||
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const { t } = useLingui()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const totalMem = toFixedFloat(chartData.systemStats.at(-1)?.stats.m ?? 0, 1)
|
||||
|
||||
@@ -60,7 +62,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
name={t`Used`}
|
||||
name={_(t`Used`)}
|
||||
order={3}
|
||||
dataKey="stats.mu"
|
||||
type="monotoneX"
|
||||
@@ -84,7 +86,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
/>
|
||||
)}
|
||||
<Area
|
||||
name={t`Cache / Buffers`}
|
||||
name={_(t`Cache / Buffers`)}
|
||||
order={1}
|
||||
dataKey="stats.mb"
|
||||
type="monotoneX"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { t } from "@lingui/macro"
|
||||
|
||||
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
@@ -18,11 +18,8 @@ import {
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
import { $temperatureFilter } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
|
||||
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
||||
const filter = useStore($temperatureFilter)
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
if (chartData.systemStats.length === 0) {
|
||||
@@ -89,28 +86,22 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + " °C"}
|
||||
filter={filter}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{colors.map((key) => {
|
||||
const filtered = filter && !key.toLowerCase().includes(filter.toLowerCase())
|
||||
let strokeOpacity = filtered ? 0.1 : 1
|
||||
return (
|
||||
<Line
|
||||
key={key}
|
||||
dataKey={key}
|
||||
name={key}
|
||||
type="monotoneX"
|
||||
dot={false}
|
||||
strokeWidth={1.5}
|
||||
stroke={newChartData.colors[key]}
|
||||
strokeOpacity={strokeOpacity}
|
||||
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{colors.map((key) => (
|
||||
<Line
|
||||
key={key}
|
||||
dataKey={key}
|
||||
name={key}
|
||||
type="monotoneX"
|
||||
dot={false}
|
||||
strokeWidth={1.5}
|
||||
stroke={newChartData.colors[key]}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
{colors.length < 12 && <ChartLegend content={<ChartLegendContent />} />}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
|
||||
@@ -19,15 +19,17 @@ import {
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command"
|
||||
import { memo, useEffect, useMemo } from "react"
|
||||
import { useEffect } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $systems } from "@/lib/stores"
|
||||
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 { getHostDisplayValue, isAdmin } from "@/lib/utils"
|
||||
import { $router, basePath, navigate } from "./router"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||
export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||
const systems = useStore($systems)
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
@@ -35,163 +37,162 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
setOpen(!open)
|
||||
}
|
||||
}
|
||||
return listen(document, "keydown", down)
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [open, setOpen])
|
||||
|
||||
return useMemo(() => {
|
||||
const systems = $systems.get()
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={t`Search for systems or settings...`} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<Trans>No results found.</Trans>
|
||||
</CommandEmpty>
|
||||
{systems.length > 0 && (
|
||||
<>
|
||||
<CommandGroup>
|
||||
{systems.map((system) => (
|
||||
<CommandItem
|
||||
key={system.id}
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "system", { name: system.name }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Server className="me-2 h-4 w-4" />
|
||||
<span>{system.name}</span>
|
||||
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator className="mb-1.5" />
|
||||
</>
|
||||
)}
|
||||
<CommandGroup heading={t`Pages / Settings`}>
|
||||
<CommandItem
|
||||
keywords={["home"]}
|
||||
onSelect={() => {
|
||||
navigate(basePath)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<LayoutDashboard className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Dashboard</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Page</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "settings", { name: "general" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<SettingsIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Settings</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Settings</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["alerts"]}
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "settings", { name: "notifications" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<MailIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Notifications</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Settings</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["help", "oauth", "oidc"]}
|
||||
onSelect={() => {
|
||||
window.location.href = "https://beszel.dev/guide/what-is-beszel"
|
||||
}}
|
||||
>
|
||||
<BookIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Documentation</Trans>
|
||||
</span>
|
||||
<CommandShortcut>beszel.dev</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<CommandSeparator className="mb-1.5" />
|
||||
<CommandGroup heading={t`Admin`}>
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={t`Search for systems or settings...`} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<Trans>No results found.</Trans>
|
||||
</CommandEmpty>
|
||||
{systems.length > 0 && (
|
||||
<>
|
||||
<CommandGroup>
|
||||
{systems.map((system) => (
|
||||
<CommandItem
|
||||
keywords={["pocketbase"]}
|
||||
key={system.id}
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "system", { name: system.name }))
|
||||
setOpen(false)
|
||||
window.open(prependBasePath("/_/"), "_blank")
|
||||
}}
|
||||
>
|
||||
<UsersIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Users</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
<Server className="me-2 h-4 w-4" />
|
||||
<span>{system.name}</span>
|
||||
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open(prependBasePath("/_/#/logs"), "_blank")
|
||||
}}
|
||||
>
|
||||
<LogsIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Logs</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open(prependBasePath("/_/#/settings/backups"), "_blank")
|
||||
}}
|
||||
>
|
||||
<DatabaseBackupIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Backups</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["email"]}
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open(prependBasePath("/_/#/settings/mail"), "_blank")
|
||||
}}
|
||||
>
|
||||
<MailIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>SMTP settings</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}, [open])
|
||||
})
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator className="mb-1.5" />
|
||||
</>
|
||||
)}
|
||||
<CommandGroup heading={t`Pages / Settings`}>
|
||||
<CommandItem
|
||||
keywords={["home"]}
|
||||
onSelect={() => {
|
||||
navigate(basePath)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<LayoutDashboard className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Dashboard</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Page</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "settings", { name: "general" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<SettingsIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Settings</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Settings</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["alerts"]}
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "settings", { name: "notifications" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<MailIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Notifications</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Settings</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["help", "oauth", "oidc"]}
|
||||
onSelect={() => {
|
||||
window.location.href = "https://beszel.dev/guide/what-is-beszel"
|
||||
}}
|
||||
>
|
||||
<BookIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Documentation</Trans>
|
||||
</span>
|
||||
<CommandShortcut>beszel.dev</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<CommandSeparator className="mb-1.5" />
|
||||
<CommandGroup heading={t`Admin`}>
|
||||
<CommandItem
|
||||
keywords={["pocketbase"]}
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open("/_/", "_blank")
|
||||
}}
|
||||
>
|
||||
<UsersIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Users</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open("/_/#/logs", "_blank")
|
||||
}}
|
||||
>
|
||||
<LogsIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Logs</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open("/_/#/settings/backups", "_blank")
|
||||
}}
|
||||
>
|
||||
<DatabaseBackupIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Backups</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["email"]}
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
window.open("/_/#/settings/mail", "_blank")
|
||||
}}
|
||||
>
|
||||
<MailIcon className="me-2 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>SMTP settings</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Admin</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { $copyContent } from "@/lib/stores"
|
||||
import { Trans } from "@lingui/macro"
|
||||
|
||||
export default function CopyToClipboard({ content }: { content: string }) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import languages from "@/lib/languages"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
|
||||
export function LangToggle() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -12,6 +10,7 @@ import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
|
||||
import { $router, Link, prependBasePath } from "../router"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const honeypot = v.literal("")
|
||||
@@ -136,6 +135,7 @@ export function UserAuthForm({
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: t`Please enable pop-ups for this site`,
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -156,17 +156,14 @@ export function UserAuthForm({
|
||||
|
||||
useEffect(() => {
|
||||
// auto login if password disabled and only one auth provider
|
||||
if (!passwordEnabled && authProviders.length === 1 && !sessionStorage.getItem("lo")) {
|
||||
// Add a small timeout to ensure browser is ready to handle popups
|
||||
setTimeout(() => {
|
||||
loginWithOauth(authProviders[0], true)
|
||||
}, 300)
|
||||
if (!passwordEnabled && authProviders.length === 1) {
|
||||
loginWithOauth(authProviders[0], true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
{passwordEnabled && (
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
{passwordEnabled && (
|
||||
<>
|
||||
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
|
||||
<div className="grid gap-2.5">
|
||||
@@ -242,20 +239,21 @@ export function UserAuthForm({
|
||||
</form>
|
||||
{(isFirstRun || oauthEnabled) && (
|
||||
// only show 'continue with' during onboarding or if we have auth providers
|
||||
(<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
<Trans>Or continue with</Trans>
|
||||
</span>
|
||||
</div>
|
||||
</div>)
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{oauthEnabled && (
|
||||
|
||||
{oauthEnabled && (
|
||||
<div className="grid gap-2 -mt-1">
|
||||
{authMethods.oauth2.providers.map((provider) => (
|
||||
<button
|
||||
@@ -285,16 +283,17 @@ export function UserAuthForm({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!oauthEnabled && isFirstRun && (
|
||||
|
||||
{!oauthEnabled && isFirstRun && (
|
||||
// only show GitHub button / dialog during onboarding
|
||||
(<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
|
||||
<img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" />
|
||||
<span className="translate-y-[1px]">GitHub</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent style={{ maxWidth: 440, width: "90%" }}>
|
||||
<DialogContent style={{ maxWidth: 440, width: "90%" }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>OAuth 2 / OIDC support</Trans>
|
||||
@@ -318,9 +317,10 @@ export function UserAuthForm({
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>)
|
||||
</Dialog>
|
||||
)}
|
||||
{passwordEnabled && !isFirstRun && (
|
||||
|
||||
{passwordEnabled && !isFirstRun && (
|
||||
<Link
|
||||
href={getPagePath($router, "forgot_password")}
|
||||
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
|
||||
@@ -328,6 +328,6 @@ export function UserAuthForm({
|
||||
<Trans>Forgot password?</Trans>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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"
|
||||
@@ -10,6 +8,7 @@ import { cn } from "@/lib/utils"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { Dialog, DialogHeader } from "../ui/dialog"
|
||||
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
|
||||
const showLoginFaliedToast = () => {
|
||||
toast({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { UserAuthForm } from "@/components/login/auth-form"
|
||||
import { Logo } from "../logo"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
@@ -7,6 +6,7 @@ import { useStore } from "@nanostores/react"
|
||||
import ForgotPassword from "./forgot-pass-form"
|
||||
import { $router } from "../router"
|
||||
import { AuthMethodsList } from "pocketbase"
|
||||
import { t } from "@lingui/macro"
|
||||
import { useTheme } from "../theme-provider"
|
||||
|
||||
export default function () {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { LaptopIcon, MoonStarIcon, SunIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useState, lazy, Suspense } from "react"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { AddSystemButton } from "./add-system"
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const CommandPalette = lazy(() => import("./command-palette"))
|
||||
|
||||
@@ -11,7 +11,7 @@ const routes = {
|
||||
* The base path of the application.
|
||||
* This is used to prepend the base path to all routes.
|
||||
*/
|
||||
export const basePath = globalThis.BESZEL.BASE_PATH || ""
|
||||
export const basePath = window.BASE_PATH || ""
|
||||
|
||||
/**
|
||||
* Prepends the base path to the given path.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense, lazy, memo, useEffect, useMemo } from "react"
|
||||
import { Suspense, lazy, useEffect, useMemo } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
|
||||
import { $alerts, $systems, pb } from "@/lib/stores"
|
||||
import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { Separator } from "../ui/separator"
|
||||
@@ -8,17 +8,17 @@ import { alertInfo, 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 { Plural, t, Trans } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
||||
|
||||
export const Home = memo(() => {
|
||||
export default function Home() {
|
||||
const hubVersion = useStore($hubVersion)
|
||||
|
||||
const alerts = useStore($alerts)
|
||||
const systems = useStore($systems)
|
||||
const { t } = useLingui()
|
||||
|
||||
let alertsKey = ""
|
||||
const activeAlerts = useMemo(() => {
|
||||
const activeAlerts = alerts.filter((alert) => {
|
||||
const active = alert.triggered && alert.name in alertInfo
|
||||
@@ -26,17 +26,14 @@ export const Home = memo(() => {
|
||||
return false
|
||||
}
|
||||
alert.sysname = systems.find((system) => system.id === alert.system)?.name
|
||||
alertsKey += alert.id
|
||||
return true
|
||||
})
|
||||
return activeAlerts
|
||||
}, [systems, alerts])
|
||||
}, [alerts])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t`Dashboard` + " / Beszel"
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
// make sure we have the latest list of systems
|
||||
updateSystemList()
|
||||
|
||||
@@ -44,6 +41,7 @@ export const Home = memo(() => {
|
||||
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
|
||||
updateRecordList(e, $systems)
|
||||
})
|
||||
// todo: add toast if new triggered alert comes in
|
||||
pb.collection<AlertRecord>("alerts").subscribe("*", (e) => {
|
||||
updateRecordList(e, $alerts)
|
||||
})
|
||||
@@ -53,15 +51,56 @@ export const Home = memo(() => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
{/* show active alerts */}
|
||||
{activeAlerts.length > 0 && <ActiveAlerts key={activeAlerts.length} activeAlerts={activeAlerts} />}
|
||||
<Suspense>
|
||||
<SystemsTable />
|
||||
</Suspense>
|
||||
return (
|
||||
<>
|
||||
{/* show active alerts */}
|
||||
{activeAlerts.length > 0 && (
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle>
|
||||
<Trans>Active Alerts</Trans>
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="max-sm:p-2">
|
||||
{activeAlerts.length > 0 && (
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3">
|
||||
{activeAlerts.map((alert) => {
|
||||
const info = alertInfo[alert.name as keyof typeof alertInfo]
|
||||
return (
|
||||
<Alert
|
||||
key={alert.id}
|
||||
className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black"
|
||||
>
|
||||
<info.icon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{alert.sysname} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { name: alert.sysname! })}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
aria-label="View system"
|
||||
></Link>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Suspense>
|
||||
<SystemsTable />
|
||||
</Suspense>
|
||||
|
||||
{hubVersion && (
|
||||
<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"
|
||||
@@ -76,56 +115,10 @@ export const Home = memo(() => {
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-foreground duration-75"
|
||||
>
|
||||
Beszel {globalThis.BESZEL.HUB_VERSION}
|
||||
Beszel {hubVersion}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
[alertsKey]
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const ActiveAlerts = memo(({ activeAlerts }: { activeAlerts: AlertRecord[] }) => {
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<CardHeader className="pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle>
|
||||
<Trans>Active Alerts</Trans>
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="max-sm:p-2">
|
||||
{activeAlerts.length > 0 && (
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3">
|
||||
{activeAlerts.map((alert) => {
|
||||
const info = alertInfo[alert.name as keyof typeof alertInfo]
|
||||
return (
|
||||
<Alert
|
||||
key={alert.id}
|
||||
className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black"
|
||||
>
|
||||
<info.icon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{alert.sysname} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { name: alert.sysname! })}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
aria-label="View system"
|
||||
></Link>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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"
|
||||
@@ -12,6 +10,7 @@ import { useState } from "react"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import clsx from "clsx"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
|
||||
export default function ConfigYaml() {
|
||||
const [configContent, setConfigContent] = useState<string>("")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
@@ -8,9 +7,10 @@ import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import { useState } from "react"
|
||||
import { Trans } from "@lingui/macro"
|
||||
import languages from "@/lib/languages"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
// import { setLang } from "@/lib/i18n"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
@@ -46,11 +46,11 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>
|
||||
Want to help improve our translations? Check{" "}
|
||||
Want to help us make our translations even better? Check out{" "}
|
||||
<a href="https://crowdin.com/project/beszel" className="link" target="_blank" rel="noopener noreferrer">
|
||||
Crowdin
|
||||
</a>{" "}
|
||||
for details.
|
||||
for more details.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { useEffect } from "react"
|
||||
import { Separator } from "../../ui/separator"
|
||||
import { SidebarNav } from "./sidebar-nav.tsx"
|
||||
@@ -14,7 +12,8 @@ import { UserSettings } from "@/types.js"
|
||||
import General from "./general.tsx"
|
||||
import Notifications from "./notifications.tsx"
|
||||
import ConfigYaml from "./config-yaml.tsx"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
try {
|
||||
@@ -45,11 +44,11 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
}
|
||||
|
||||
export default function SettingsLayout() {
|
||||
const { t } = useLingui()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: t({ message: `General`, comment: "Context: General settings" }),
|
||||
title: _(t({ message: `General`, comment: "Context: General settings" })),
|
||||
href: getPagePath($router, "settings", { name: "general" }),
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
@@ -14,6 +12,7 @@ import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import * as v from "valibot"
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Plural, Trans } from "@lingui/react/macro"
|
||||
import {
|
||||
$systems,
|
||||
pb,
|
||||
$chartTime,
|
||||
$containerFilter,
|
||||
$userSettings,
|
||||
$direction,
|
||||
$maxValues,
|
||||
$temperatureFilter,
|
||||
} from "@/lib/stores"
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings, $direction, $maxValues } from "@/lib/stores"
|
||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||
import { ChartType, Os } from "@/lib/enums"
|
||||
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||
import { useStore } from "@nanostores/react"
|
||||
@@ -24,7 +12,6 @@ import {
|
||||
getHostDisplayValue,
|
||||
getPbTimestamp,
|
||||
getSizeAndUnit,
|
||||
listen,
|
||||
toFixedFloat,
|
||||
useLocalStorage,
|
||||
} from "@/lib/utils"
|
||||
@@ -32,13 +19,12 @@ import { Separator } from "../ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||
import { Button } from "../ui/button"
|
||||
import { Input } from "../ui/input"
|
||||
import { ChartAverage, ChartMax, Rows, TuxIcon, WindowsIcon, AppleIcon } from "../ui/icons"
|
||||
import { ChartAverage, ChartMax, Rows, TuxIcon } from "../ui/icons"
|
||||
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||
import { timeTicks } from "d3-time"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { $router, navigate } from "../router"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { Plural, Trans, t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
|
||||
const AreaChartDefault = lazy(() => import("../charts/area-chart"))
|
||||
const ContainerChart = lazy(() => import("../charts/container-chart"))
|
||||
@@ -117,7 +103,7 @@ function dockerOrPodman(str: string, system: SystemRecord) {
|
||||
|
||||
export default function SystemDetail({ name }: { name: string }) {
|
||||
const direction = useStore($direction)
|
||||
const { t } = useLingui()
|
||||
const { _ } = useLingui()
|
||||
const systems = useStore($systems)
|
||||
const chartTime = useStore($chartTime)
|
||||
const maxValues = useStore($maxValues)
|
||||
@@ -126,7 +112,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
|
||||
const netCardRef = useRef<HTMLDivElement>(null)
|
||||
const persistChartTime = useRef(false)
|
||||
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
|
||||
const [bottomSpacing, setBottomSpacing] = useState(0)
|
||||
const [chartLoading, setChartLoading] = useState(true)
|
||||
@@ -135,10 +120,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
useEffect(() => {
|
||||
document.title = `${name} / Beszel`
|
||||
return () => {
|
||||
if (!persistChartTime.current) {
|
||||
$chartTime.set($userSettings.get().chartTime)
|
||||
}
|
||||
persistChartTime.current = false
|
||||
$chartTime.set($userSettings.get().chartTime)
|
||||
// resetCharts()
|
||||
setSystemStats([])
|
||||
setContainerData([])
|
||||
setContainerFilterBar(null)
|
||||
@@ -228,7 +211,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
cache.set(cs_cache_key, containerData)
|
||||
}
|
||||
if (containerData.length) {
|
||||
!containerFilterBar && setContainerFilterBar(<FilterBar />)
|
||||
!containerFilterBar && setContainerFilterBar(<ContainerFilterBar />)
|
||||
} else if (containerFilterBar) {
|
||||
setContainerFilterBar(null)
|
||||
}
|
||||
@@ -261,23 +244,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
if (!system.info) {
|
||||
return []
|
||||
}
|
||||
|
||||
const osInfo = {
|
||||
[Os.Linux]: {
|
||||
Icon: TuxIcon,
|
||||
value: system.info.k,
|
||||
label: t({ comment: "Linux kernel", message: "Kernel" }),
|
||||
},
|
||||
[Os.Darwin]: {
|
||||
Icon: AppleIcon,
|
||||
value: `macOS ${system.info.k}`,
|
||||
},
|
||||
[Os.Windows]: {
|
||||
Icon: WindowsIcon,
|
||||
value: system.info.k,
|
||||
},
|
||||
}
|
||||
|
||||
let uptime: React.ReactNode
|
||||
if (system.info.u < 172800) {
|
||||
const hours = Math.trunc(system.info.u / 3600)
|
||||
@@ -294,8 +260,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
// hide if hostname is same as host or name
|
||||
hide: system.info.h === system.host || system.info.h === system.name,
|
||||
},
|
||||
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
|
||||
osInfo[system.info.os ?? Os.Linux],
|
||||
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime` },
|
||||
{ value: system.info.k, Icon: TuxIcon, label: t({ comment: "Linux kernel", message: "Kernel" }) },
|
||||
{
|
||||
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
|
||||
Icon: CpuIcon,
|
||||
@@ -323,41 +289,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
setBottomSpacing(tooltipHeight - distanceToBottom)
|
||||
}, [netCardRef, containerData])
|
||||
|
||||
// keyboard navigation between systems
|
||||
useEffect(() => {
|
||||
if (!systems.length) {
|
||||
return
|
||||
}
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.shiftKey ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
const currentIndex = systems.findIndex((s) => s.name === name)
|
||||
if (currentIndex === -1 || systems.length <= 1) {
|
||||
return
|
||||
}
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
case "h":
|
||||
const prevIndex = (currentIndex - 1 + systems.length) % systems.length
|
||||
persistChartTime.current = true
|
||||
return navigate(getPagePath($router, "system", { name: systems[prevIndex].name }))
|
||||
case "ArrowRight":
|
||||
case "l":
|
||||
const nextIndex = (currentIndex + 1) % systems.length
|
||||
persistChartTime.current = true
|
||||
return navigate(getPagePath($router, "system", { name: systems[nextIndex].name }))
|
||||
}
|
||||
}
|
||||
return listen(document, "keyup", handleKeyUp)
|
||||
}, [name, systems])
|
||||
|
||||
if (!system.id) {
|
||||
return null
|
||||
}
|
||||
@@ -464,7 +395,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`CPU Usage`}
|
||||
title={_(t`CPU Usage`)}
|
||||
description={t`Average system-wide CPU utilization`}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
@@ -479,7 +410,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
description={t`Average CPU utilization of containers`}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart chartData={chartData} dataKey="c" chartType={ChartType.CPU} />
|
||||
<ContainerChart chartData={chartData} dataKey="c" chartName="cpu" />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
@@ -500,7 +431,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart chartData={chartData} dataKey="m" chartType={ChartType.Memory} />
|
||||
<ContainerChart chartData={chartData} chartName="mem" dataKey="m" unit=" MB" />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
@@ -542,7 +473,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<ContainerChart chartData={chartData} chartType={ChartType.Network} dataKey="n" />
|
||||
<ContainerChart chartData={chartData} chartName="net" dataKey="n" />
|
||||
</ChartCard>
|
||||
</div>
|
||||
)}
|
||||
@@ -566,7 +497,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
grid={grid}
|
||||
title={t`Temperature`}
|
||||
description={t`Temperatures of system sensors`}
|
||||
cornerEl={<FilterBar store={$temperatureFilter} />}
|
||||
>
|
||||
<TemperatureChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
@@ -590,10 +520,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<div className="grid xl:grid-cols-2 gap-4">
|
||||
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
|
||||
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData
|
||||
const sizeFormatter = (value: number, decimals?: number) => {
|
||||
const { v, u } = getSizeAndUnit(value, false)
|
||||
return toFixedFloat(v, decimals || 1) + u
|
||||
}
|
||||
return (
|
||||
<div key={id} className="contents">
|
||||
<ChartCard
|
||||
@@ -613,9 +539,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName={`g.${id}.mu`}
|
||||
unit=" MB"
|
||||
max={gpu.mt}
|
||||
tickFormatter={sizeFormatter}
|
||||
contentFormatter={(value) => sizeFormatter(value, 2)}
|
||||
tickFormatter={(value) => {
|
||||
const { v, u } = getSizeAndUnit(value, false)
|
||||
return toFixedFloat(v, 1) + u
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
</div>
|
||||
@@ -664,17 +593,17 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
|
||||
const containerFilter = useStore(store)
|
||||
const { t } = useLingui()
|
||||
function ContainerFilterBar() {
|
||||
const containerFilter = useStore($containerFilter)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
store.set(e.target.value)
|
||||
$containerFilter.set(e.target.value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input placeholder={t`Filter...`} className="ps-4 pe-8" value={containerFilter} onChange={handleChange} />
|
||||
<Input placeholder={_(t`Filter...`)} className="ps-4 pe-8" value={containerFilter} onChange={handleChange} />
|
||||
{containerFilter && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -682,7 +611,7 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
|
||||
size="icon"
|
||||
aria-label="Clear"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
|
||||
onClick={() => store.set("")}
|
||||
onClick={() => $containerFilter.set("")}
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
HeaderContext,
|
||||
Row,
|
||||
Table as TableType,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
@@ -63,13 +61,14 @@ import {
|
||||
PenBoxIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { $systems, pb } from "@/lib/stores"
|
||||
import { $hubVersion, $systems, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
||||
import AlertsButton from "../alerts/alert-button"
|
||||
import { $router, Link, navigate } from "../router"
|
||||
import { EthernetIcon, GpuIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { useLingui, Trans } from "@lingui/react/macro"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import { Input } from "../ui/input"
|
||||
import { ClassValue } from "clsx"
|
||||
@@ -104,66 +103,47 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
|
||||
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
|
||||
const { column } = context
|
||||
// @ts-ignore
|
||||
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-9 px-3 flex"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{Icon && <Icon className="me-2 size-4" />}
|
||||
{name()}
|
||||
{hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef.icon && <column.columnDef.icon className="me-2 size-4" />}
|
||||
{column.id}
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef.hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SystemsTable() {
|
||||
const data = useStore($systems)
|
||||
const { i18n, t } = useLingui()
|
||||
const hubVersion = useStore($hubVersion)
|
||||
const [filter, setFilter] = useState<string>()
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "system", desc: false }])
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: t`System`, desc: false }])
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
|
||||
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
|
||||
|
||||
const locale = i18n.locale
|
||||
const { i18n } = useLingui()
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== undefined) {
|
||||
table.getColumn("system")?.setFilterValue(filter)
|
||||
table.getColumn(t`System`)?.setFilterValue(filter)
|
||||
}
|
||||
}, [filter])
|
||||
|
||||
const columnDefs = useMemo(() => {
|
||||
const statusTranslations = {
|
||||
up: () => t`Up`.toLowerCase(),
|
||||
down: () => t`Down`.toLowerCase(),
|
||||
paused: () => t`Paused`.toLowerCase(),
|
||||
}
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
// size: 200,
|
||||
size: 200,
|
||||
minSize: 0,
|
||||
accessorKey: "name",
|
||||
id: "system",
|
||||
name: () => t`System`,
|
||||
filterFn: (row, _, filterVal) => {
|
||||
const filterLower = filterVal.toLowerCase()
|
||||
const { name, status } = row.original
|
||||
// Check if the filter matches the name or status for this row
|
||||
if (
|
||||
name.toLowerCase().includes(filterLower) ||
|
||||
statusTranslations[status as keyof typeof statusTranslations]?.().includes(filterLower)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
id: t`System`,
|
||||
enableHiding: false,
|
||||
Icon: ServerIcon,
|
||||
icon: ServerIcon,
|
||||
cell: (info) => (
|
||||
<span className="flex gap-0.5 items-center text-base md:pe-5">
|
||||
<IndicatorDot system={info.row.original} />
|
||||
@@ -182,48 +162,43 @@ export default function SystemsTable() {
|
||||
},
|
||||
{
|
||||
accessorKey: "info.cpu",
|
||||
id: "cpu",
|
||||
name: () => t`CPU`,
|
||||
id: t`CPU`,
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
Icon: CpuIcon,
|
||||
icon: CpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.mp",
|
||||
id: "memory",
|
||||
name: () => t`Memory`,
|
||||
id: t`Memory`,
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
Icon: MemoryStickIcon,
|
||||
icon: MemoryStickIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.dp",
|
||||
id: "disk",
|
||||
name: () => t`Disk`,
|
||||
id: t`Disk`,
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
Icon: HardDriveIcon,
|
||||
icon: HardDriveIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.g,
|
||||
id: "gpu",
|
||||
name: () => "GPU",
|
||||
id: "GPU",
|
||||
invertSorting: true,
|
||||
sortUndefined: -1,
|
||||
cell: CellFormatter,
|
||||
Icon: GpuIcon,
|
||||
icon: GpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.b || 0,
|
||||
id: "net",
|
||||
name: () => t`Net`,
|
||||
id: t`Net`,
|
||||
invertSorting: true,
|
||||
size: 50,
|
||||
Icon: EthernetIcon,
|
||||
icon: EthernetIcon,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
const val = info.getValue() as number
|
||||
@@ -240,13 +215,15 @@ export default function SystemsTable() {
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.dt,
|
||||
id: "temp",
|
||||
name: () => t({ message: "Temp", comment: "Temperature label in systems table" }),
|
||||
id: t({
|
||||
message: "Temp",
|
||||
comment: "Temperature label in systems table",
|
||||
}),
|
||||
invertSorting: true,
|
||||
sortUndefined: -1,
|
||||
size: 50,
|
||||
hideSort: true,
|
||||
Icon: ThermometerIcon,
|
||||
icon: ThermometerIcon,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
const val = info.getValue() as number
|
||||
@@ -266,16 +243,15 @@ export default function SystemsTable() {
|
||||
},
|
||||
{
|
||||
accessorKey: "info.v",
|
||||
id: "agent",
|
||||
name: () => t`Agent`,
|
||||
id: t`Agent`,
|
||||
invertSorting: true,
|
||||
size: 50,
|
||||
Icon: WifiIcon,
|
||||
icon: WifiIcon,
|
||||
hideSort: true,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
const version = info.getValue() as string
|
||||
if (!version) {
|
||||
if (!version || !hubVersion) {
|
||||
return null
|
||||
}
|
||||
const system = info.row.original
|
||||
@@ -289,7 +265,7 @@ export default function SystemsTable() {
|
||||
system={system}
|
||||
className={
|
||||
(system.status !== "up" && "bg-primary/30") ||
|
||||
(version === globalThis.BESZEL.HUB_VERSION && "bg-green-500") ||
|
||||
(version === hubVersion && "bg-green-500") ||
|
||||
"bg-yellow-500"
|
||||
}
|
||||
/>
|
||||
@@ -299,9 +275,7 @@ export default function SystemsTable() {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
// @ts-ignore
|
||||
name: () => t({ message: "Actions", comment: "Table column" }),
|
||||
id: t({ message: "Actions", comment: "Table column" }),
|
||||
size: 50,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end items-center gap-1">
|
||||
@@ -311,11 +285,11 @@ export default function SystemsTable() {
|
||||
),
|
||||
},
|
||||
] as ColumnDef<SystemRecord>[]
|
||||
}, [])
|
||||
}, [hubVersion, i18n.locale])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: columnDefs,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
@@ -329,17 +303,15 @@ export default function SystemsTable() {
|
||||
},
|
||||
defaultColumn: {
|
||||
minSize: 0,
|
||||
size: 900,
|
||||
maxSize: 900,
|
||||
size: Number.MAX_SAFE_INTEGER,
|
||||
maxSize: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
})
|
||||
|
||||
const rows = table.getRowModel().rows
|
||||
const columns = table.getAllColumns()
|
||||
const visibleColumns = table.getVisibleLeafColumns()
|
||||
// TODO: hiding temp then gpu messes up table headers
|
||||
const CardHead = useMemo(() => {
|
||||
return (
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<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">
|
||||
@@ -390,8 +362,8 @@ export default function SystemsTable() {
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-1 pb-1">
|
||||
{columns.map((column) => {
|
||||
if (!column.getCanSort()) return null
|
||||
{table.getAllColumns().map((column) => {
|
||||
if (column.id === t`Actions` || !column.getCanSort()) return null
|
||||
let Icon = <span className="w-6"></span>
|
||||
// if current sort column, show sort direction
|
||||
if (sorting[0]?.id === column.id) {
|
||||
@@ -410,8 +382,7 @@ export default function SystemsTable() {
|
||||
key={column.id}
|
||||
>
|
||||
{Icon}
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef.name()}
|
||||
{column.id}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
@@ -425,7 +396,8 @@ export default function SystemsTable() {
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-1.5 pb-1">
|
||||
{columns
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
@@ -435,8 +407,7 @@ export default function SystemsTable() {
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef.name()}
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)
|
||||
})}
|
||||
@@ -448,24 +419,128 @@ export default function SystemsTable() {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
)
|
||||
}, [visibleColumns.length, sorting, viewMode, locale])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{CardHead}
|
||||
<div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
|
||||
{viewMode === "table" ? (
|
||||
// table layout
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead className="px-2" key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.original.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className={cn("cursor-pointer transition-opacity", {
|
||||
"opacity-50": row.original.status === "paused",
|
||||
})}
|
||||
onClick={(e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
|
||||
navigate(getPagePath($router, "system", { name: row.original.name }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize() === Number.MAX_SAFE_INTEGER ? "auto" : cell.column.getSize(),
|
||||
}}
|
||||
className={cn("overflow-hidden relative", data.length > 10 ? "py-2" : "py-2.5")}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
<Trans>No systems found.</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
// grid layout
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{rows?.length ? (
|
||||
rows.map((row) => {
|
||||
return <SystemCard key={row.original.id} row={row} table={table} colLength={visibleColumns.length} />
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => {
|
||||
const system = row.original
|
||||
const { status } = system
|
||||
return (
|
||||
<Card
|
||||
key={system.id}
|
||||
className={cn(
|
||||
"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
|
||||
{
|
||||
"opacity-50": status === "paused",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
|
||||
<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} />
|
||||
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
|
||||
{system.name}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardTitle>
|
||||
{table.getColumn(t`Actions`)?.getIsVisible() && (
|
||||
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
||||
<AlertsButton system={system} />
|
||||
<ActionsButton system={system} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2.5 text-sm px-5 pt-3.5 pb-4">
|
||||
{table.getAllColumns().map((column) => {
|
||||
if (!column.getIsVisible() || column.id === t`System` || column.id === t`Actions`) return null
|
||||
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
|
||||
if (!cell) return null
|
||||
return (
|
||||
<div key={column.id} className="flex items-center gap-3">
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef?.icon && (
|
||||
// @ts-ignore
|
||||
<column.columnDef.icon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<span className="text-muted-foreground min-w-16">{column.id}:</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 })}
|
||||
className="inset-0 absolute w-full h-full"
|
||||
>
|
||||
<span className="sr-only">{row.original.name}</span>
|
||||
</Link>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="col-span-full text-center py-8">
|
||||
@@ -479,247 +554,6 @@ export default function SystemsTable() {
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead className="px-2" key={header.id}>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
)
|
||||
}, [i18n.locale, colLength])
|
||||
}
|
||||
|
||||
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", {
|
||||
"opacity-50": system.status === "paused",
|
||||
})}
|
||||
onClick={(e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
|
||||
navigate(getPagePath($router, "system", { name: system.name }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
}}
|
||||
className={cn("overflow-hidden relative", 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 }) => {
|
||||
const system = row.original
|
||||
const { t } = useLingui()
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<Card
|
||||
key={system.id}
|
||||
className={cn(
|
||||
"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
|
||||
{
|
||||
"opacity-50": system.status === "paused",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
|
||||
<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} />
|
||||
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
|
||||
{system.name}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardTitle>
|
||||
{table.getColumn("actions")?.getIsVisible() && (
|
||||
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
||||
<AlertsButton system={system} />
|
||||
<ActionsButton system={system} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-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 })}
|
||||
className="inset-0 absolute w-full h-full"
|
||||
>
|
||||
<span className="sr-only">{row.original.name}</span>
|
||||
</Link>
|
||||
</Card>
|
||||
)
|
||||
}, [system, colLength, t])
|
||||
}
|
||||
)
|
||||
|
||||
const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
let editOpened = useRef(false)
|
||||
const { t } = useLingui()
|
||||
const { id, status, host, name } = system
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size={"icon"} data-nolink>
|
||||
<span className="sr-only">
|
||||
<Trans>Open menu</Trans>
|
||||
</span>
|
||||
<MoreHorizontalIcon className="w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{!isReadOnlyUser() && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
editOpened.current = true
|
||||
setEditOpen(true)
|
||||
}}
|
||||
>
|
||||
<PenBoxIcon className="me-2.5 size-4" />
|
||||
<Trans>Edit</Trans>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className={cn(isReadOnlyUser() && "hidden")}
|
||||
onClick={() => {
|
||||
pb.collection("systems").update(id, {
|
||||
status: status === "paused" ? "pending" : "paused",
|
||||
})
|
||||
}}
|
||||
>
|
||||
{status === "paused" ? (
|
||||
<>
|
||||
<PlayCircleIcon className="me-2.5 size-4" />
|
||||
<Trans>Resume</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PauseCircleIcon className="me-2.5 size-4" />
|
||||
<Trans>Pause</Trans>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
|
||||
<CopyIcon className="me-2.5 size-4" />
|
||||
<Trans>Copy host</Trans>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")} onSelect={() => setDeleteOpen(true)}>
|
||||
<Trash2Icon className="me-2.5 size-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* edit dialog */}
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}
|
||||
</Dialog>
|
||||
{/* deletion dialog */}
|
||||
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Are you sure you want to delete {name}?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>
|
||||
This action cannot be undone. This will permanently delete all current records for {name} from the
|
||||
database.
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={() => pb.collection("systems").delete(id)}
|
||||
>
|
||||
<Trans>Continue</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}, [id, status, host, name, t, deleteOpen, editOpen])
|
||||
})
|
||||
|
||||
function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) {
|
||||
className ||= {
|
||||
"bg-green-500": system.status === "up",
|
||||
@@ -734,3 +568,99 @@ function IndicatorDot({ system, className }: { system: SystemRecord; className?:
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
let editOpened = useRef(false)
|
||||
|
||||
const { id, status, host, name } = system
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size={"icon"} data-nolink>
|
||||
<span className="sr-only">
|
||||
<Trans>Open menu</Trans>
|
||||
</span>
|
||||
<MoreHorizontalIcon className="w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{!isReadOnlyUser() && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
editOpened.current = true
|
||||
setEditOpen(true)
|
||||
}}
|
||||
>
|
||||
<PenBoxIcon className="me-2.5 size-4" />
|
||||
<Trans>Edit</Trans>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className={cn(isReadOnlyUser() && "hidden")}
|
||||
onClick={() => {
|
||||
pb.collection("systems").update(id, {
|
||||
status: status === "paused" ? "pending" : "paused",
|
||||
})
|
||||
}}
|
||||
>
|
||||
{status === "paused" ? (
|
||||
<>
|
||||
<PlayCircleIcon className="me-2.5 size-4" />
|
||||
<Trans>Resume</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PauseCircleIcon className="me-2.5 size-4" />
|
||||
<Trans>Pause</Trans>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
|
||||
<CopyIcon className="me-2.5 size-4" />
|
||||
<Trans>Copy host</Trans>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")} onSelect={() => setDeleteOpen(true)}>
|
||||
<Trash2Icon className="me-2.5 size-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* edit dialog */}
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}
|
||||
</Dialog>
|
||||
{/* deletion dialog */}
|
||||
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Are you sure you want to delete {name}?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>
|
||||
This action cannot be undone. This will permanently delete all current records for {name} from the
|
||||
database.
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={() => pb.collection("systems").delete(id)}
|
||||
>
|
||||
<Trans>Continue</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -99,7 +99,6 @@ const ChartTooltipContent = React.forwardRef<
|
||||
unit?: string
|
||||
filter?: string
|
||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||
truncate?: boolean
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -120,7 +119,6 @@ const ChartTooltipContent = React.forwardRef<
|
||||
filter,
|
||||
itemSorter,
|
||||
contentFormatter: content = undefined,
|
||||
truncate = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -129,7 +127,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
|
||||
React.useMemo(() => {
|
||||
if (filter) {
|
||||
payload = payload?.filter((item) => (item.name as string)?.toLowerCase().includes(filter.toLowerCase()))
|
||||
payload = payload?.filter((item) => (item.name as string)?.includes(filter))
|
||||
}
|
||||
if (itemSorter) {
|
||||
// @ts-ignore
|
||||
@@ -216,15 +214,10 @@ const ChartTooltipContent = React.forwardRef<
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground",
|
||||
truncate ? "max-w-40 truncate leading-normal -my-1" : ""
|
||||
)}
|
||||
>
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
||||
</div>
|
||||
{item.value !== undefined && (
|
||||
<span className="font-medium tabular-nums text-foreground">
|
||||
{content && typeof content === "function"
|
||||
|
||||
@@ -12,42 +12,6 @@ export function TuxIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
// icon park (Apache 2.0) https://github.com/bytedance/IconPark/blob/master/LICENSE
|
||||
export function WindowsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 48 48">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3.8"
|
||||
d="m6.8 11 12.9-1.7v12.1h-13zm18-2.2 16.4-2v14.6H25zm0 18.6 16.4.4v13.4L25 38.6zm-18-.8 12.9.3v10.9l-13-2.2z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// teenyicons (MIT) https://github.com/teenyicons/teenyicons/blob/master/LICENSE
|
||||
export function AppleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.1 4.7a5 5 0 0 1 3.8 2c-3.3 1.9-2.8 6.7.6 8L17.2 17c-.8 1.3-2 2.9-3.5 2.9-1.2 0-1.6-.9-3.3-.8s-2.2.8-3.5.8c-1.4 0-2.5-1.5-3.4-2.7-2.3-3.6-2.5-7.9-1.1-10 1-1.7 2.6-2.6 4.1-2.6 1.6 0 2.6.8 3.8.8 1.3 0 2-.8 3.8-.8M13.7 0c.2 1.2-.3 2.4-1 3.2a4 4 0 0 1-3 1.6c-.2-1.2.3-2.3 1-3.2.7-.8 2-1.5 3-1.6"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// ion icons (MIT) https://github.com/ionic-team/ionicons/blob/main/LICENSE
|
||||
export function DockerIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 512 512" fill="currentColor">
|
||||
<path d="M507 211c-1-1-14-11-42-11a133 133 0 0 0-21 2c-6-36-36-54-37-55l-7-4-5 7a102 102 0 0 0-13 30c-5 21-2 40 8 57-12 7-33 9-37 9H16a16 16 0 0 0-16 16 241 241 0 0 0 15 87c11 30 29 53 51 67 25 15 66 24 113 24a344 344 0 0 0 62-6 257 257 0 0 0 82-29 224 224 0 0 0 55-46c27-30 43-64 55-94h4c30 0 48-12 58-22a63 63 0 0 0 15-22l2-6Z" />
|
||||
<path d="M47 236h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4H47a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m62 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m-125-57h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m62 0h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m0-58h45a4 4 0 0 0 4-4V76a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 116h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// MingCute Apache License 2.0 https://github.com/Richard9394/MingCute
|
||||
export function Rows(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export enum Os {
|
||||
Linux = 0,
|
||||
Darwin,
|
||||
Windows,
|
||||
// FreeBSD,
|
||||
}
|
||||
|
||||
export enum ChartType {
|
||||
Memory,
|
||||
Disk,
|
||||
Network,
|
||||
CPU,
|
||||
}
|
||||
@@ -3,7 +3,15 @@ import { i18n } from "@lingui/core"
|
||||
import type { Messages } from "@lingui/core"
|
||||
import languages from "@/lib/languages"
|
||||
import { detect, fromStorage, fromNavigator } from "@lingui/detect-locale"
|
||||
import { messages as enMessages } from "@/locales/en/en"
|
||||
import { messages as enMessages } from "@/locales/en/en.ts"
|
||||
|
||||
// let locale = detect(fromUrl("lang"), fromStorage("lang"), fromNavigator(), "en")
|
||||
let locale = detect(fromStorage("lang"), fromNavigator(), "en")
|
||||
|
||||
// log if dev
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("detected locale", locale)
|
||||
}
|
||||
|
||||
// activates locale
|
||||
function activateLocale(locale: string, messages: Messages = enMessages) {
|
||||
@@ -29,28 +37,21 @@ export async function dynamicActivate(locale: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
// let locale = detect(fromUrl("lang"), fromStorage("lang"), fromNavigator(), "en")
|
||||
let locale = detect(fromStorage("lang"), fromNavigator(), "en")
|
||||
// log if dev
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("detected locale", locale)
|
||||
}
|
||||
// handle zh variants
|
||||
if (locale?.startsWith("zh-")) {
|
||||
// map zh variants to zh-CN
|
||||
const zhVariantMap: Record<string, string> = {
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh",
|
||||
"zh-MO": "zh",
|
||||
"zh-Hant": "zh",
|
||||
}
|
||||
return zhVariantMap[locale] || "zh-CN"
|
||||
// handle zh variants
|
||||
if (locale?.startsWith("zh-")) {
|
||||
// map zh variants to zh-CN
|
||||
const zhVariantMap: Record<string, string> = {
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh",
|
||||
"zh-MO": "zh",
|
||||
"zh-Hant": "zh",
|
||||
}
|
||||
dynamicActivate(zhVariantMap[locale] || "zh-CN")
|
||||
} else {
|
||||
locale = (locale || "en").split("-")[0]
|
||||
// use en if locale is not in languages
|
||||
if (!languages.some((l) => l.lang === locale)) {
|
||||
locale = "en"
|
||||
}
|
||||
return locale
|
||||
dynamicActivate(locale)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ export const $alerts = atom([] as AlertRecord[])
|
||||
/** SSH public key */
|
||||
export const $publicKey = atom("")
|
||||
|
||||
/** Beszel hub version */
|
||||
export const $hubVersion = atom("")
|
||||
|
||||
/** Chart time period */
|
||||
export const $chartTime = atom("1h") as PreinitializedWritableAtom<ChartTimes>
|
||||
|
||||
@@ -38,9 +41,6 @@ $userSettings.subscribe((value) => {
|
||||
/** Container chart filter */
|
||||
export const $containerFilter = atom("")
|
||||
|
||||
/** Temperature chart filter */
|
||||
export const $temperatureFilter = atom("")
|
||||
|
||||
/** Fallback copy to clipboard dialog content */
|
||||
export const $copyContent = atom("")
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
@@ -10,21 +9,13 @@ import { timeDay, timeHour } from "d3-time"
|
||||
import { useEffect, useState } from "react"
|
||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||
import { EthernetIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||
import { t } from "@lingui/macro"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/** Adds event listener to node and returns function that removes the listener */
|
||||
export function listen<T extends Event = Event>(
|
||||
node: Node,
|
||||
event: string,
|
||||
handler: (event: T) => void
|
||||
) {
|
||||
node.addEventListener(event, handler as EventListener)
|
||||
return () => node.removeEventListener(event, handler as EventListener)
|
||||
}
|
||||
// export const cn = clsx
|
||||
|
||||
export async function copyToClipboard(content: string) {
|
||||
const duration = 1500
|
||||
@@ -77,7 +68,6 @@ export const updateSystemList = (() => {
|
||||
|
||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||
export async function logOut() {
|
||||
sessionStorage.setItem("lo", "t")
|
||||
pb.authStore.clear()
|
||||
pb.realtime.unsubscribe()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ar\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:49\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"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 يومًا"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "إجراءات"
|
||||
|
||||
@@ -70,15 +71,12 @@ msgstr "إضافة نظام"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Add URL"
|
||||
msgstr "إضافة عنوان URL"
|
||||
msgstr "إضافة رابط"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "تعديل خيارات العرض للرسوم البيانية."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -121,7 +119,7 @@ msgstr "المتوسط يتجاوز <0>{value}{0}</0>"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average power consumption of GPUs"
|
||||
msgstr "متوسط استهلاك طاقة GPUs"
|
||||
msgstr "متوسط استهلاك طاقة وحدة معالجة الرسوميات"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average system-wide CPU utilization"
|
||||
@@ -144,7 +142,7 @@ msgstr "عرض النطاق الترددي"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "يدعم Beszel OpenID Connect والعديد من مزودي المصادقة OAuth2."
|
||||
msgstr "يدعم بيزيل بروتوكول OpenID Connect والعديد من مزوّدي المصادقة عبر بروتوكول OAuth2."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
|
||||
@@ -186,7 +184,7 @@ msgstr "تحقق من السجلات لمزيد من التفاصيل."
|
||||
msgid "Check your notification service"
|
||||
msgstr "تحقق من خدمة الإشعارات الخاصة بك"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "انقر للنسخ"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "تم النسخ إلى الحافظة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "نسخ docker compose"
|
||||
msgstr "نسخ أمر تركيب الدوكر"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "نسخ docker run"
|
||||
msgstr "نسخ أمر تشغيل الدوكر"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "نسخ متغيرات البيئة"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "نسخ المضيف"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "نسخ أمر لينكس"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "نسخ أمر لينكس"
|
||||
msgid "Copy text"
|
||||
msgstr "نسخ النص"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "انسخ أمر التثبيت للوكيل أدناه، أو سجل الوكلاء تلقائياً باستخدام <0>رمز مميز عالمي</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "انسخ محتوى <0>docker-compose.yml</0> للوكيل أدناه، أو سجل الوكلاء تلقائياً باستخدام <1>رمز مميز عالمي</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "نسخ YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "المعالج"
|
||||
@@ -266,6 +284,10 @@ msgstr "الفترة الزمنية الافتراضية"
|
||||
msgid "Delete"
|
||||
msgstr "حذف"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "حذف البصمة"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "القرص"
|
||||
@@ -286,15 +308,15 @@ msgstr "استخدام القرص لـ {extraFsName}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker CPU Usage"
|
||||
msgstr "استخدام المعالج لـ Docker"
|
||||
msgstr "استخدام المعالج للدوكر"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Memory Usage"
|
||||
msgstr "استخدام الذاكرة لـ Docker"
|
||||
msgstr "استخدام الذاكرة للدوكر"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Network I/O"
|
||||
msgstr "إدخال/إخراج الشبكة لـ Docker"
|
||||
msgstr "إدخال/إخراج الشبكة للدوكر"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Documentation"
|
||||
@@ -329,6 +351,7 @@ msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيي
|
||||
msgid "Enter email address..."
|
||||
msgstr "أدخل عنوان البريد الإشباكي..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "فشل في تحديث التنبيه"
|
||||
msgid "Filter..."
|
||||
msgstr "تصفية..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}"
|
||||
@@ -392,13 +419,14 @@ msgstr "عام"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
msgstr "استهلاك طاقة GPU"
|
||||
msgstr "استهلاك طاقة وحدة معالجة الرسوميات"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Grid"
|
||||
msgstr "شبكة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "أمر Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "عنوان البريد الإشباكي غير صالح."
|
||||
msgid "Kernel"
|
||||
msgstr "النواة"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "اللغة"
|
||||
@@ -433,6 +471,14 @@ msgstr "التخطيط"
|
||||
msgid "Light"
|
||||
msgstr "فاتح"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "تسجيل الخروج"
|
||||
@@ -466,7 +512,7 @@ msgstr "تعليمات الإعداد اليدوي"
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Max 1 min"
|
||||
msgstr "1 دقيقة كحد"
|
||||
msgstr "الحد الأقصى دقيقة"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Memory"
|
||||
@@ -479,7 +525,7 @@ msgstr "استخدام الذاكرة"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "استخدام الذاكرة لحاويات Docker"
|
||||
msgstr "استخدام الذاكرة لحاويات دوكر"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Name"
|
||||
@@ -491,7 +537,7 @@ msgstr "الشبكة"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
msgstr "حركة مرور الشبكة لحاويات Docker"
|
||||
msgstr "حركة مرور الشبكة لحاويات الدوكر"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of public interfaces"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "في كل إعادة تشغيل، سيتم تحديث الأنظمة في قاعدة البيانات لتتطابق مع الأنظمة المعرفة في الملف."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "فتح القائمة"
|
||||
|
||||
@@ -563,7 +611,7 @@ msgstr "إيقاف مؤقت"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused"
|
||||
msgstr "متوقف مؤقتًا"
|
||||
msgstr "متوقف مؤقتا"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -635,6 +683,10 @@ msgstr "إعادة تعيين كلمة المرور"
|
||||
msgid "Resume"
|
||||
msgstr "استئناف"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "تدوير الرمز المميز"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي."
|
||||
@@ -669,7 +721,6 @@ msgstr "تم الإرسال"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "يحدد النطاق الزمني الافتراضي للرسوم البيانية عند عرض النظام."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "استخدام التبديل"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "النظام"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "اختبار <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "تم إرسال إشعار الاختبار"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "يجب أن يكون الوكيل قيد التشغيل على النظام للاتصال. انسخ أمر التثبيت للوكيل أدناه."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "يجب أن يكون الوكيل قيد التشغيل على النظام للاتصال. انسخ <0>docker-compose.yml</0> للوكيل أدناه."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "ثم قم بتسجيل الدخول إلى الواجهة الخلفية وأعد تعيين كلمة مرور حساب المستخدم الخاص بك في جدول المستخدمين."
|
||||
@@ -783,9 +827,36 @@ msgstr "تبديل الشبكة"
|
||||
msgid "Toggle theme"
|
||||
msgstr "تبديل السمة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "رمز مميز"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "الرموز المميزة والبصمات"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "تسمح الرموز المميزة للوكلاء بالاتصال والتسجيل. البصمات هي معرفات مستقرة فريدة لكل نظام، يتم تعيينها عند الاتصال الأول."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
|
||||
msgstr "يتم التفعيل عندما <EFBFBD><EFBFBD>تجاوز أي مستشعر عتبة معينة"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
@@ -807,6 +878,10 @@ msgstr "يتم التفعيل عندما يتغير الحالة بين التش
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "رمز مميز عالمي"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,10 +937,15 @@ msgstr "هل تريد مساعدتنا في تحسين ترجماتنا؟ تحق
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "إشعارات Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "عند التفعيل، يسمح هذا الرمز المميز للوكلاء بالتسجيل الذاتي دون إنشاء نظام مسبق. ينتهي بعد ساعة واحدة أو عند إعادة تشغيل المحور."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "أمر Windows"
|
||||
msgstr "أمر ويندوز"
|
||||
|
||||
#. Disk write
|
||||
#: src/components/charts/area-chart.tsx
|
||||
@@ -884,3 +964,4 @@ msgstr "تكوين YAML"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: bg\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:49\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 дни"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Действия"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Добави URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Настрой опциите за показване на диаграмите."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Провери log-овете за повече информация."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Провери услугата си за удостоверяване"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Настисни за да копираш"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Записано в клипборда"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Копирай docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Копирай docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Копирай хоста"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Копирай linux командата"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Копирай linux командата"
|
||||
msgid "Copy text"
|
||||
msgstr "Копирай текста"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Процесор"
|
||||
@@ -266,6 +284,10 @@ msgstr "Времеви диапазон по подразбиране"
|
||||
msgid "Delete"
|
||||
msgstr "Изтрий"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Диск"
|
||||
@@ -329,6 +351,7 @@ msgstr "Въведи имейл адрес за да нулираш парола
|
||||
msgid "Enter email address..."
|
||||
msgstr "Въведи имейл адрес..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Неуспешно обнови тревога"
|
||||
msgid "Filter..."
|
||||
msgstr "Филтрирай..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Мрежово"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Команда Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Невалиден имейл адрес."
|
||||
msgid "Kernel"
|
||||
msgstr "Linux Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Език"
|
||||
@@ -433,6 +471,14 @@ msgstr "Подреждане"
|
||||
msgid "Light"
|
||||
msgstr "Светъл"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Изход"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "На всеки рестарт, системите в датабазата ще бъдат обновени да съвпадат със системите зададени във файла."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Отвори менюто"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Нулиране на парола"
|
||||
msgid "Resume"
|
||||
msgstr "Възобнови"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл."
|
||||
@@ -669,7 +721,6 @@ msgstr "Изпратени"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Задава диапазона за време за диаграмите, когато се разглежда система."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Използване на swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Тествай <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Тестова нотификация изпратена"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Агента трябва да работи на системата за да се свърже. Копирай инсталационната команда за агента долу."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Агемта трябва да работи на системата за да се свърже. Копирай <0>docker-compose.yml</0> файла за агента долу."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "След това влез в backend-а и нулирай паролата за потребителския акаунт в таблицата за потребители."
|
||||
@@ -783,6 +827,33 @@ msgstr "Превключване на мрежа"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Включи тема"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Задейства се, когато някой даден сензор надвиши зададен праг"
|
||||
@@ -807,6 +878,10 @@ msgstr "Задейства се, когато статуса превключв
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Задейства се, когато употребата на някой диск надивши зададен праг"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Искаш да помогнеш да направиш преводит
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Пуш нотификации"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Команда Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML конфигурация"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Настройките за потребителя ти са обновени."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: cs\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-14 00:50\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:49\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dní"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Akce"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Přidat URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Upravit možnosti zobrazení pro grafy."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Pro více informací zkontrolujte logy."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Zkontrolujte službu upozornění"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Klikněte pro zkopírování"
|
||||
|
||||
@@ -213,13 +211,20 @@ msgid "Copied to clipboard"
|
||||
msgstr "Zkopírováno do schránky"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopírovat docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Zkopírovat příkaz na spuštění dockeru"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
@@ -227,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopírovat hostitele"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopírovat příkaz Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopírovat příkaz Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopírovat text"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Procesor"
|
||||
@@ -266,6 +284,10 @@ msgstr "Výchozí doba"
|
||||
msgid "Delete"
|
||||
msgstr "Odstranit"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -329,6 +351,7 @@ msgstr "Zadejte e-mailovou adresu pro obnovu hesla"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Zadejte e-mailovou adresu..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Nepodařilo se aktualizovat upozornění"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtr..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}"
|
||||
@@ -399,9 +426,10 @@ msgid "Grid"
|
||||
msgstr "Mřížka"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr ""
|
||||
msgstr "Homebrew příkaz"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
@@ -420,6 +448,16 @@ msgstr "Neplatná e-mailová adresa."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jazyk"
|
||||
@@ -433,6 +471,14 @@ msgstr "Rozvržení"
|
||||
msgid "Light"
|
||||
msgstr "Světlý"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Odhlásit"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Při každém restartu budou systémy v databázi aktualizovány tak, aby odpovídaly systémům definovaným v souboru."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Otevřít menu"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Obnovit heslo"
|
||||
msgid "Resume"
|
||||
msgstr "Pokračovat"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mailových oznámení ponechte prázdné pole."
|
||||
@@ -669,7 +721,6 @@ msgstr "Odeslat"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Nastaví výchozí časový rozsah grafů, když je systém zobrazen."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap využití"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Systém"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testovací oznámení odesláno"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Agent musí být v systému spuštěn, aby se mohl připojit. Zkopírujte níže uvedený instalační příkaz pro agenta."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Agent musí být v systému spuštěn, aby se mohl připojit. Zkopírujte níže uvedený soubor<0>docker-compose.yml</0> pro agenta."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Poté se přihlaste do backendu a obnovte heslo k uživatelskému účtu v tabulce uživatelů."
|
||||
@@ -783,6 +827,33 @@ msgstr "Přepnout mřížku"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Přepnout motiv"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Spustí se, když některý senzor překročí prahovou hodnotu"
|
||||
@@ -807,6 +878,10 @@ msgstr "Spouští se, když se změní dostupnost"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,10 +937,15 @@ msgstr "Chcete nám pomoci s našimi překlady ještě lépe? Podívejte se na <
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push oznámení"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr ""
|
||||
msgstr "Windows příkaz"
|
||||
|
||||
#. Disk write
|
||||
#: src/components/charts/area-chart.tsx
|
||||
@@ -884,3 +964,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."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: da\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dage"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Handlinger"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Tilføj URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Juster visningsindstillinger for diagrammer."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Tjek logfiler for flere detaljer."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Tjek din notifikationstjeneste"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Klik for at kopiere"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopieret til udklipsholder"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiér docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiér docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopier host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopier Linux kommando"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopier Linux kommando"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopier tekst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Standard tidsperiode"
|
||||
msgid "Delete"
|
||||
msgstr "Slet"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -305,12 +327,12 @@ msgstr "Dokumentation"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
msgstr "Nede"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Rediger"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -329,6 +351,7 @@ msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Indtast e-mailadresse..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Kunne ikke opdatere alarm"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "For <0>{min}</0> {min, plural, one {minut} other {minutter}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Gitter"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-kommando"
|
||||
@@ -420,6 +448,16 @@ msgstr "Ugyldig email adresse."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprog"
|
||||
@@ -433,6 +471,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Lys"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Log ud"
|
||||
@@ -461,7 +507,7 @@ msgstr "Administrer display og notifikationsindstillinger."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
msgstr "Manuel opsætningsvejledning"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Ved hver genstart vil systemer i databasen blive opdateret til at matche de systemer, der er defineret i filen."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Åbn menu"
|
||||
|
||||
@@ -551,7 +599,7 @@ msgstr "Adgangskoden skal være på mindst 8 tegn."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be less than 72 bytes."
|
||||
msgstr ""
|
||||
msgstr "Adgangskoden skal være mindre end 72 bytes."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Password reset request received"
|
||||
@@ -635,6 +683,10 @@ msgstr "Nulstil adgangskode"
|
||||
msgid "Resume"
|
||||
msgstr "Genoptag"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at deaktivere e-mail-meddelelser."
|
||||
@@ -646,7 +698,7 @@ msgstr "Gem indstillinger"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr ""
|
||||
msgstr "Gem system"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -669,7 +721,6 @@ msgstr "Sendt"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Sætter standardtidsintervallet for diagrammer når et system vises."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap forbrug"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -727,7 +779,7 @@ msgstr "Tabel"
|
||||
#. Temperature label in systems table
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
msgstr "Temperatur"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Test notifikation sendt"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Agenten skal køre på systemet for at forbinde. Kopier installationskommandoen for agenten nedenfor."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Agenten skal køre på systemet for at forbinde. Kopier <0>docker-compose.yml</0> for agenten nedenfor."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Log derefter ind på backend og nulstil adgangskoden til din brugerkonto i tabellen brugere."
|
||||
@@ -783,6 +827,33 @@ msgstr "Slå gitter til/fra"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Skift tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Udløser når en sensor overstiger en tærskel"
|
||||
@@ -807,11 +878,15 @@ msgstr "Udløser når status skifter mellem op og ned"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Udløser når brugen af en disk overstiger en tærskel"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
msgstr "Oppe"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Updated in real time. Click on a system to view information."
|
||||
@@ -862,7 +937,12 @@ msgstr "Vil du hjælpe os med at gøre vores oversættelser endnu bedre? Tjek <0
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push notifikationer"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-kommando"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML Konfiguration"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dine brugerindstillinger er opdateret."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 20:24\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 Tage"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "URL hinzufügen"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Anzeigeoptionen für Diagramme anpassen."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Überprüfe die Protokolle für weitere Details."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Überprüfe deinen Benachrichtigungsdienst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Zum Kopieren klicken"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "In die Zwischenablage kopiert"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose kopieren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run kopieren"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Umgebungsvariablen kopieren"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Host kopieren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linux-Befehl kopieren"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Linux-Befehl kopieren"
|
||||
msgid "Copy text"
|
||||
msgstr "Text kopieren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Kopieren Sie den Installationsbefehl für den Agent unten oder registrieren Sie Agents automatisch mit einem <0>universellen Token</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Kopieren Sie den<0>docker-compose.yml</0> Inhalt für den Agent unten oder registrieren Sie Agents automatisch mit einem <1>universellen Token</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "YAML kopieren"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Standardzeitraum"
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Fingerabdruck löschen"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Festplatte"
|
||||
@@ -305,7 +327,7 @@ msgstr "Dokumentation"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
msgstr "Offline"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
@@ -329,6 +351,7 @@ msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen"
|
||||
msgid "Enter email address..."
|
||||
msgstr "E-Mail-Adresse eingeben..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Warnung konnte nicht aktualisiert werden"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Fingerabdruck"
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Raster"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-Befehl"
|
||||
@@ -420,6 +448,16 @@ msgstr "Ungültige E-Mail-Adresse."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr "L15"
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr "L5"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
@@ -433,6 +471,14 @@ msgstr "Anordnung"
|
||||
msgid "Light"
|
||||
msgstr "Hell"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr "Durchschnittliche Systemlast 15 Min"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr "Durchschnittliche Systemlast 5 Min"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Abmelden"
|
||||
@@ -461,7 +507,7 @@ msgstr "Anzeige- und Benachrichtigungseinstellungen verwalten."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
msgstr "Anleitung zur manuellen Einrichtung"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Menü öffnen"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Passwort zurücksetzen"
|
||||
msgid "Resume"
|
||||
msgstr "Fortsetzen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Token rotieren"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren."
|
||||
@@ -646,7 +698,7 @@ msgstr "Einstellungen speichern"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr ""
|
||||
msgstr "System sichern"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -669,7 +721,6 @@ msgstr "Gesendet"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Legt den Standardzeitraum für Diagramme fest, wenn ein System angezeigt wird."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap-Nutzung"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -727,7 +779,7 @@ msgstr "Tabelle"
|
||||
#. Temperature label in systems table
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
msgstr "Temperatur"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testbenachrichtigung gesendet"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Der Agent muss auf dem System laufen, um eine Verbindung herzustellen. Kopiere den Installationsbefehl für den Agent unten."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Der Agent muss auf dem System laufen, um eine Verbindung herzustellen. Kopiere die <0>docker-compose.yml</0> für den Agent unten."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Melde dich dann im Backend an und setze dein Benutzerkontopasswort in der Benutzertabelle zurück."
|
||||
@@ -783,6 +827,33 @@ msgstr "Raster umschalten"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Darstellung umschalten"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Tokens & Fingerabdrücke"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fingerabdrücke sind stabile, eindeutige Identifikatoren für jedes System, die bei der ersten Verbindung gesetzt werden."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 15 Minuten einen Schwellenwert überschreitet"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwellenwert überschreitet"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet"
|
||||
@@ -807,11 +878,15 @@ msgstr "Löst aus, wenn der Status zwischen online und offline wechselt"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Universeller Token"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
msgstr "aktiv"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Updated in real time. Click on a system to view information."
|
||||
@@ -862,7 +937,12 @@ msgstr "Möchtest du uns helfen, unsere Übersetzungen noch besser zu machen? Sc
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push-Benachrichtigungen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Wenn aktiviert, ermöglicht dieser Token Agents, sich selbst zu registrieren, ohne vorherige Systemerstellung. Läuft nach einer Stunde oder beim Hub-Neustart ab."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-Befehl"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML-Konfiguration"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
||||
|
||||
|
||||
967
beszel/site/src/locales/el/el.po
Normal file
967
beszel/site/src/locales/el/el.po
Normal file
@@ -0,0 +1,967 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2024-11-01 11:30-0400\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: el\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Greek\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: el\n"
|
||||
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 16\n"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# day} other {# days}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{hours, plural, one {# hour} other {# hours}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 week"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "12 hours"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "24 hours"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "30 days"
|
||||
msgstr ""
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
msgstr "Προσθήκη <0>System</0>"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add New System"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add system"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Add URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Agent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
msgid "Alerts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
msgid "All Systems"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Are you sure you want to delete {name}?"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/copy-to-clipboard.tsx
|
||||
msgid "Automatic copy requires a secure context."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: data.alert.unit
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average power consumption of GPUs"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average system-wide CPU utilization"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: gpu.n
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Backups"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Bandwidth"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Binary"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change general application options."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Chart options"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Check {email} for a reset link."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Check logs for more details."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Check your notification service"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Command line instructions"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Configure how you receive alert notifications."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Copied to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/copy-to-clipboard.tsx
|
||||
msgid "Copy text"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "CPU Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Create account"
|
||||
msgstr ""
|
||||
|
||||
#. Dark theme
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Default time period"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Disk I/O"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/charts/disk-chart.tsx
|
||||
msgid "Disk Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Disk usage of {extraFsName}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker CPU Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Network I/O"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Documentation"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is down
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Email notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Enter email address to reset password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Enter email address..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export your current systems configuration."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Failed to save settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Failed to send test notification"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "Failed to update alert"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Filter..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr ""
|
||||
|
||||
#. Context: General settings
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "General"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr ""
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
#. Light theme
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Login attempt failed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Manage display and notification preferences."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Max 1 min"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Memory"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Net"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of public interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "No results found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "OAuth 2 / OIDC support"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Or continue with"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Pages / Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be at least 8 characters."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be less than 72 bytes."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Password reset request received"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Pause"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "Please check logs for more details."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Please check your credentials and try again"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Please create an admin account"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Please enable pop-ups for this site"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Please log in again"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Please see <0>the documentation</0> for instructions."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Please sign in to your account"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Preferred Language"
|
||||
msgstr ""
|
||||
|
||||
#. Use 'Key' if your language requires many more characters
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Public Key"
|
||||
msgstr ""
|
||||
|
||||
#. Disk read
|
||||
#: src/components/charts/area-chart.tsx
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "Read"
|
||||
msgstr ""
|
||||
|
||||
#. Network bytes received (download)
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "Received"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Reset Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Save Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Search for systems or settings..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
msgid "See <0>notification settings</0> to configure how you receive alerts."
|
||||
msgstr ""
|
||||
|
||||
#. Network bytes sent (upload)
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "Sent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings saved"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "SMTP settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Sort By"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Swap space used by the system"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Swap Usage"
|
||||
msgstr ""
|
||||
|
||||
#. System theme
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Table"
|
||||
msgstr ""
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Temperature"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Temperatures of system sensors"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Test <0>URL</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Test notification sent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Throughput of {extraFsName}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Throughput of root filesystem"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "To email(s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when CPU usage exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when memory usage exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when status switches between up and down"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Updated in real time. Click on a system to view information."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Uptime"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "Usage"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Usage of root partition"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/charts/swap-chart.tsx
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "Used"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Visible Fields"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Waiting for enough records to display"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr ""
|
||||
|
||||
#. Disk write
|
||||
#: src/components/charts/area-chart.tsx
|
||||
#: src/components/charts/area-chart.tsx
|
||||
msgid "Write"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "YAML Config"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "YAML Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:49\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 días"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Acciones"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Agregar URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Ajustar las opciones de visualización para los gráficos."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Revise los registros para más detalles."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Verifique su servicio de notificaciones"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Haga clic para copiar"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copiado al portapapeles"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copiar docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copiar docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Copiar env"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Copiar host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copiar comando de Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Copiar comando de Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Copiar texto"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Copia el comando de instalación del agente a continuación, o registra agentes automáticamente con un <0>token universal</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Copia el contenido del<0>docker-compose.yml</0> para el agente a continuación, o registra agentes automáticamente con un <1>token universal</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copiar YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Período de tiempo predeterminado"
|
||||
msgid "Delete"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Eliminar huella digital"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disco"
|
||||
@@ -329,6 +351,7 @@ msgstr "Ingrese la dirección de correo electrónico para restablecer la contras
|
||||
msgid "Enter email address..."
|
||||
msgstr "Ingrese dirección de correo..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Error al actualizar la alerta"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrar..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Cuadrícula"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Comando Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Dirección de correo electrónico no válida."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
@@ -433,6 +471,14 @@ msgstr "Diseño"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Cerrar Sesión"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "En cada reinicio, los sistemas en la base de datos se actualizarán para coincidir con los sistemas definidos en el archivo."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Abrir menú"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Restablecer Contraseña"
|
||||
msgid "Resume"
|
||||
msgstr "Reanudar"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Rotar token"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Guarde la dirección usando la tecla enter o coma. Deje en blanco para desactivar las notificaciones por correo."
|
||||
@@ -669,7 +721,6 @@ msgstr "Enviado"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Establece el rango de tiempo predeterminado para los gráficos cuando se visualiza un sistema."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Uso de Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Probar <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Notificación de prueba enviada"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "El agente debe estar ejecutándose en el sistema para conectarse. Copie el comando de instalación para el agente a continuación."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "El agente debe estar ejecutándose en el sistema para conectarse. Copie el <0>docker-compose.yml</0> para el agente a continuación."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Luego inicie sesión en el backend y restablezca la contraseña de su cuenta de usuario en la tabla de usuarios."
|
||||
@@ -783,6 +827,33 @@ msgstr "Alternar cuadrícula"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Alternar tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Tokens y Huellas Digitales"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Los tokens permiten que los agentes se conecten y registren. Las huellas digitales son identificadores estables únicos para cada sistema, establecidos en la primera conexión."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Los tokens y las huellas digitales se utilizan para autenticar las conexiones WebSocket al hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Se activa cuando cualquier sensor supera un umbral"
|
||||
@@ -807,6 +878,10 @@ msgstr "Se activa cuando el estado cambia entre activo e inactivo"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Se activa cuando el uso de cualquier disco supera un umbral"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Token universal"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "¿Quieres ayudarnos a mejorar nuestras traducciones? Consulta <0>Crowdin
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Notificaciones Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Cuando está habilitado, este token permite que los agentes se auto-registren sin crear previamente el sistema. Expira después de una hora o al reiniciar el hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Comando Windows"
|
||||
@@ -884,3 +964,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."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fa\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Persian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "۳۰ روز"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "عملیات"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "افزودن آدرس اینترنتی"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "تنظیم گزینههای نمایش برای نمودارها."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "برای جزئیات بیشتر، لاگها را بررسی کنی
|
||||
msgid "Check your notification service"
|
||||
msgstr "سرویس اطلاعرسانی خود را بررسی کنید"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "برای کپی کردن کلیک کنید"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "در کلیپبورد کپی شد"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "کپی docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "کپی docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "کپی متغیرهای محیط"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "کپی میزبان"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "کپی دستور لینوکس"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "کپی دستور لینوکس"
|
||||
msgid "Copy text"
|
||||
msgstr "کپی متن"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "دستور نصب عامل زیر را کپی کنید، یا عاملها را به طور خودکار با <0>توکن جهانی</0> ثبت کنید."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "محتوای <0>docker-compose.yml</0> عامل زیر را کپی کنید، یا عاملها را به طور خودکار با <1>توکن جهانی</1> ثبت کنید."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "کپی YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "پردازنده"
|
||||
@@ -266,6 +284,10 @@ msgstr "بازه زمانی پیشفرض"
|
||||
msgid "Delete"
|
||||
msgstr "حذف"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "حذف اثر انگشت"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "دیسک"
|
||||
@@ -329,6 +351,7 @@ msgstr "آدرس ایمیل را برای بازنشانی رمز عبور وا
|
||||
msgid "Enter email address..."
|
||||
msgstr "آدرس ایمیل را وارد کنید..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "بهروزرسانی هشدار ناموفق بود"
|
||||
msgid "Filter..."
|
||||
msgstr "فیلتر..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "برای <0>{min}</0> {min, plural, one {دقیقه} other {دقیقه}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "جدول"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "دستور Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "آدرس ایمیل نامعتبر است."
|
||||
msgid "Kernel"
|
||||
msgstr "هسته"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "زبان"
|
||||
@@ -433,6 +471,14 @@ msgstr "طرحبندی"
|
||||
msgid "Light"
|
||||
msgstr "روشن"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "خروج"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "در هر بار راهاندازی مجدد، سیستمهای موجود در پایگاه داده با سیستمهای تعریف شده در فایل مطابقت داده میشوند."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "باز کردن منو"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "بازنشانی رمز عبور"
|
||||
msgid "Resume"
|
||||
msgstr "ادامه"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "چرخش توکن"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "آدرس را با استفاده از کلید Enter یا کاما ذخیره کنید. برای غیرفعال کردن اعلانهای ایمیلی، خالی بگذارید."
|
||||
@@ -669,7 +721,6 @@ msgstr "ارسال شد"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "بازه زمانی پیشفرض برای نمودارها هنگام مشاهده یک سیستم را تعیین میکند."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "میزان استفاده از Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "سیستم"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "تست <0>آدرس اینترنتی</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "اعلان آزمایشی ارسال شد"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "برای اتصال، عامل باید روی سیستم در حال اجرا باشد. دستور نصب عامل را از زیر کپی کنید."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "برای اتصال، عامل باید روی سیستم در حال اجرا باشد. <0>docker-compose.yml</0> مربوط به عامل را از زیر کپی کنید."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "سپس وارد بخش پشتیبان شوید و رمز عبور حساب کاربری خود را در جدول کاربران بازنشانی کنید."
|
||||
@@ -783,6 +827,33 @@ msgstr "تغییر نمایش جدول"
|
||||
msgid "Toggle theme"
|
||||
msgstr "تغییر تم"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "توکن"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "توکنها و اثرات انگشت"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "توکنها به عاملها اجازه اتصال و ثبتنام میدهند. اثرات انگشت شناسههای پایدار منحصر به فرد هر سیستم هستند که در اولین اتصال تنظیم میشوند."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "توکنها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده میشوند."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "هنگامی که هر حسگری از یک آستانه فراتر رود، فعال میشود"
|
||||
@@ -807,6 +878,10 @@ msgstr "هنگامی که وضعیت بین بالا و پایین تغییر م
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "هنگامی که استفاده از هر دیسکی از یک آستانه فراتر رود، فعال میشود"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "توکن جهانی"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "میخواهید به ما کمک کنید تا ترجمههای
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "اعلانهای Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "هنگامی که فعال است، این توکن به عاملها اجازه خودثبتنامی بدون ایجاد سیستم قبلی میدهد. پس از یک ساعت یا در راهاندازی مجدد هاب منقضی میشود."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "دستور Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "پیکربندی YAML"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:49\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 jours"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Ajouter URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Ajuster les options d'affichage pour les graphiques."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Vérifiez les journaux pour plus de détails."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Vérifiez votre service de notification"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Cliquez pour copier"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copié dans le presse-papiers"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copier docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copier docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Copier env"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Copier l'hôte"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copier la commande Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Copier la commande Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Copier le texte"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Copiez la commande d'installation de l'agent ci-dessous, ou enregistrez les agents automatiquement avec un <0>token universel</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Copiez le contenu du<0>docker-compose.yml</0> pour l'agent ci-dessous, ou enregistrez les agents automatiquement avec un <1>token universel</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copier YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Période par défaut"
|
||||
msgid "Delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Supprimer l'empreinte"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disque"
|
||||
@@ -329,6 +351,7 @@ msgstr "Entrez l'adresse email pour réinitialiser le mot de passe"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Entrez l'adresse email..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Échec de la mise à jour de l'alerte"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrer..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Pour <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Grille"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Commande Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Adresse email invalide."
|
||||
msgid "Kernel"
|
||||
msgstr "Noyau"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
@@ -433,6 +471,14 @@ msgstr "Disposition"
|
||||
msgid "Light"
|
||||
msgstr "Clair"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Déconnexion"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "À chaque redémarrage, les systèmes dans la base de données seront mis à jour pour correspondre aux systèmes définis dans le fichier."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Ouvrir le menu"
|
||||
|
||||
@@ -559,7 +607,7 @@ msgstr "Demande de réinitialisation du mot de passe reçue"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Pause"
|
||||
msgstr "En pause"
|
||||
msgstr "Pause"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused"
|
||||
@@ -635,6 +683,10 @@ msgstr "Réinitialiser le mot de passe"
|
||||
msgid "Resume"
|
||||
msgstr "Reprendre"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Faire tourner le token"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Enregistrez l'adresse en utilisant la touche Entrée ou la virgule. Laissez vide pour désactiver les notifications par email."
|
||||
@@ -669,7 +721,6 @@ msgstr "Envoyé"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Définit la plage de temps par défaut pour les graphiques lorsqu'un système est consulté."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Utilisation du swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Système"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Tester <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Notification de test envoyée"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "L'agent doit être en cours d'exécution sur le système pour se connecter. Copiez la commande d'installation pour l'agent ci-dessous."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "L'agent doit être en cours d'exécution sur le système pour se connecter. Copiez le <0>docker-compose.yml</0> pour l'agent ci-dessous."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Ensuite, connectez-vous au backend et réinitialisez le mot de passe de votre compte utilisateur dans la table des utilisateurs."
|
||||
@@ -783,6 +827,33 @@ msgstr "Basculer la grille"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Changer le thème"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Tokens et Empreintes"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Les tokens permettent aux agents de se connecter et de s'enregistrer. Les empreintes sont des identifiants stables uniques à chaque système, définis lors de la première connexion."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Les tokens et les empreintes sont utilisés pour authentifier les connexions WebSocket vers le hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Déclenchement lorsque tout capteur dépasse un seuil"
|
||||
@@ -807,6 +878,10 @@ msgstr "Se déclenche lorsque le statut passe de \"Joignable\" à \"Injoignable\
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Token universel"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Vous voulez nous aider à améliorer nos traductions ? Consultez <0>Crow
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Notifications Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Lorsqu'il est activé, ce token permet aux agents de s'auto-enregistrer sans création préalable du système. Expire après une heure ou au redémarrage du hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Commande Windows"
|
||||
@@ -884,3 +964,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."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\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"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dana"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Akcije"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Dodaj URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Podesite opcije prikaza za grafikone."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Provjerite logove za više detalja."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Provjerite Vaš servis notifikacija"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Pritisnite za kopiranje"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopirano u međuspremnik"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiraj docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiraj docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopiraj hosta"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiraj Linux komandu"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopiraj Linux komandu"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopiraj tekst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Procesor"
|
||||
@@ -266,6 +284,10 @@ msgstr "Zadano vremensko razdoblje"
|
||||
msgid "Delete"
|
||||
msgstr "Izbriši"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -329,6 +351,7 @@ msgstr "Unesite email adresu za resetiranje lozinke"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Unesite email adresu..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Ažuriranje upozorenja nije uspjelo"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minutu} other {minute}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Mreža"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew naredba"
|
||||
@@ -420,6 +448,16 @@ msgstr "Nevažeća adresa e-pošte."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jezik"
|
||||
@@ -433,6 +471,14 @@ msgstr "Izgled"
|
||||
msgid "Light"
|
||||
msgstr "Svijetlo"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Odjava"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka biti će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Otvori menu"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Resetiraj Lozinku"
|
||||
msgid "Resume"
|
||||
msgstr "Nastavi"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Spremite adresu pomoću tipke enter ili zareza. Ostavite prazno kako biste onemogućili obavijesti e-poštom."
|
||||
@@ -669,7 +721,6 @@ msgstr "Poslano"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Postavlja zadani vremenski raspon za grafikone kada se sustav gleda."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap Iskorištenost"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistem"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Testni <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testna obavijest poslana"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Agent mora biti pokrenut na sistemu da bi se spojio. Kopirajte instalacijske komande za agenta ispod."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Agent mora biti pokrenut na sistemu da bi se spojio. Kopirajte <0>docker-compose.yml</0> za agenta ispod."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Zatim se prijavite u backend i resetirajte lozinku korisničkog računa u tablici korisnika."
|
||||
@@ -783,6 +827,33 @@ msgstr "Uključi/isključi rešetku"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Uključi/isključi temu"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Pokreće se kada bilo koji senzor prijeđe prag"
|
||||
@@ -807,6 +878,10 @@ msgstr "Pokreće se kada se status sistema promijeni"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Pokreće se kada iskorištenost bilo kojeg diska premaši prag"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Želite li nam pomoći da naše prijevode učinimo još boljim? Posjetit
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push obavijest"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows naredba"
|
||||
@@ -884,3 +964,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."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hu\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 nap"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Műveletek"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "URL hozzáadása"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Állítsa be a diagram megjelenítését."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ 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/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Kattints a másoláshoz"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Vágólapra másolva"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose másolása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run másolása"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Hoszt másolása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linux parancs másolása"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Linux parancs másolása"
|
||||
msgid "Copy text"
|
||||
msgstr "Szöveg másolása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Alapértelmezett időszak"
|
||||
msgid "Delete"
|
||||
msgstr "Törlés"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Lemez"
|
||||
@@ -329,6 +351,7 @@ msgstr "E-mail cím megadása a jelszó visszaállításához"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Adja meg az e-mail címet..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Nem sikerült frissíteni a riasztást"
|
||||
msgid "Filter..."
|
||||
msgstr "Szűrő..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "A <0>{min}</0> {min, plural, one {perc} other {percek}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Rács"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew parancs"
|
||||
@@ -420,6 +448,16 @@ msgstr "Érvénytelen e-mail cím."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Nyelv"
|
||||
@@ -433,6 +471,14 @@ msgstr "Elrendezés"
|
||||
msgid "Light"
|
||||
msgstr "Világos"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Kijelentkezés"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Minden újraindításkor az adatbázisban lévő rendszerek frissítésre kerülnek, hogy megfeleljenek a fájlban meghatározott rendszereknek."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Menü megnyitása"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Jelszó visszaállítása"
|
||||
msgid "Resume"
|
||||
msgstr "Folytatás"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Mentse el a címet az Enter billentyű vagy a vessző használatával. Hagyja üresen az e-mail értesítések letiltásához."
|
||||
@@ -669,7 +721,6 @@ msgstr "Elküldve"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Beállítja az alapértelmezett időtartamot a diagramokhoz, amikor egy rendszert néznek."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap használat"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Rendszer"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Teszt <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Teszt értesítés elküldve"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "A csatlakozáshoz az ügynöknek futnia kell a rendszerben. Másolja ki az alábbi telepítési parancsot az ügynök telepítéséhez."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "A csatlakozáshoz az ügynöknek futnia kell a rendszerben. Másolja az<0>docker-compose.yml</0> fájlt az ügynök futtatásához."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Ezután jelentkezzen be a backendbe, és állítsa vissza a felhasználói fiók jelszavát a felhasználók táblázatban."
|
||||
@@ -783,6 +827,33 @@ msgstr "Rács ki- és bekapcsolása"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Téma váltása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
|
||||
@@ -807,6 +878,10 @@ msgstr "Bekapcsol, amikor az állapot fel és le között változik"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha a lemez érzékelő túllép egy küszöbértéket"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Szeretne segíteni nekünk abban, hogy fordításaink még jobbak legyen
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push értesítések"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows parancs"
|
||||
@@ -884,3 +964,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."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: is\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Icelandic\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dagar"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Aðgerðir"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Bæta við léni"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ 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/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Smelltu til að afrita"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Afritað í klippiborð"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Afrita docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Afrita docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Afrita host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Afrita Linux aðgerð"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Afrita Linux aðgerð"
|
||||
msgid "Copy text"
|
||||
msgstr "Afrita texta"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Örgjörvi"
|
||||
@@ -266,6 +284,10 @@ msgstr "Sjálfgefið tímabil"
|
||||
msgid "Delete"
|
||||
msgstr "Eyða"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Diskur"
|
||||
@@ -329,6 +351,7 @@ msgstr "Settu netfang til að endursetja lykilorð"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Settu inn Netfang..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Mistókst að uppfæra tilkynningu"
|
||||
msgid "Filter..."
|
||||
msgstr "Sía..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr ""
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew skipun"
|
||||
@@ -420,6 +448,16 @@ msgstr "Ógilt netfang."
|
||||
msgid "Kernel"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Tungumál"
|
||||
@@ -433,6 +471,14 @@ msgstr ""
|
||||
msgid "Light"
|
||||
msgstr "Ljóst"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Útskrá"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Opna valmynd"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Endurstilla lykilorð"
|
||||
msgid "Resume"
|
||||
msgstr "Halda áfram"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr ""
|
||||
@@ -669,7 +721,6 @@ msgstr "Sent"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Skipti minni"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Kerfi"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Prufa <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Prufu tilkynning send"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Skráðu þig þá inní bakendann og endurstilltu lykilorðið þitt inni í notenda töflunni."
|
||||
@@ -783,6 +827,33 @@ msgstr ""
|
||||
msgid "Toggle theme"
|
||||
msgstr "Velja þema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Virkjast þegar einhver skynjari fer yfir þröskuld"
|
||||
@@ -807,6 +878,10 @@ msgstr "Virkjast þegar staða breytist milli virkur og óvirkur"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Virkjast þegar diska notkun fer yfir þröskuld"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr ""
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Tilkynningar"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows skipun"
|
||||
@@ -884,3 +964,4 @@ msgstr ""
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Notenda stillingar vistaðar."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-15 07:05\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 giorni"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Azioni"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Aggiungi URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Regola le opzioni di visualizzazione per i grafici."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Controlla i log per maggiori dettagli."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Controlla il tuo servizio di notifica"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Clicca per copiare"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copiato negli appunti"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copia docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copia docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Copia env"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Copia host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copia comando Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Copia comando Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Copia testo"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Copia il comando di installazione per l'agente qui sotto, o registra gli agenti automaticamente con un <0>token universale</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Copia il contenuto<0>docker-compose.yml</0> per l'agente qui sotto, o registra gli agenti automaticamente con un <1>token universale</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copia YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Periodo di tempo predefinito"
|
||||
msgid "Delete"
|
||||
msgstr "Elimina"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Elimina impronta digitale"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disco"
|
||||
@@ -329,6 +351,7 @@ msgstr "Inserisci l'indirizzo email per reimpostare la password"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Inserisci l'indirizzo email..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Aggiornamento dell'avviso fallito"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtra..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Per <0>{min}</0> {min, plural, one {minuto} other {minuti}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Griglia"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Comando Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Indirizzo email non valido."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Lingua"
|
||||
@@ -433,6 +471,14 @@ msgstr "Aspetto"
|
||||
msgid "Light"
|
||||
msgstr "Chiaro"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr "Caricamento medio 15m"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr "Caricamento medio 5m"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Disconnetti"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Ad ogni riavvio, i sistemi nel database verranno aggiornati per corrispondere ai sistemi definiti nel file."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Apri menu"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Reimposta Password"
|
||||
msgid "Resume"
|
||||
msgstr "Riprendi"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Ruota token"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Salva l'indirizzo usando il tasto invio o la virgola. Lascia vuoto per disabilitare le notifiche email."
|
||||
@@ -669,7 +721,6 @@ msgstr "Inviato"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Imposta l'intervallo di tempo predefinito per i grafici quando viene visualizzato un sistema."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Utilizzo Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Notifica di test inviata"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "L'agente deve essere in esecuzione sul sistema per connettersi. Copia il comando di installazione per l'agente qui sotto."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "L'agente deve essere in esecuzione sul sistema per connettersi. Copia il<0>docker-compose.yml</0> per l'agente qui sotto."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Quindi accedi al backend e reimposta la password del tuo account utente nella tabella degli utenti."
|
||||
@@ -783,6 +827,33 @@ msgstr "Attiva/disattiva griglia"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Attiva/disattiva tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Token e Impronte Digitali"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "I token consentono agli agenti di connettersi e registrarsi. Le impronte digitali sono identificatori stabili unici per ogni sistema, impostati alla prima connessione."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "I token e le impronte digitali vengono utilizzati per autenticare le connessioni WebSocket all'hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Attiva quando un sensore supera una soglia"
|
||||
@@ -807,6 +878,10 @@ msgstr "Attiva quando lo stato passa tra up e down"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Attiva quando l'utilizzo di un disco supera una soglia"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Token universale"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Vuoi aiutarci a migliorare ulteriormente le nostre traduzioni? Dai un'oc
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Notifiche Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Quando abilitato, questo token consente agli agenti di auto-registrarsi senza creazione preventiva del sistema. Scade dopo un'ora o al riavvio dell'hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Comando Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "Configurazione YAML"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Le impostazioni utente sono state aggiornate."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-13 10:13\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30日間"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "アクション"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "URLを追加"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "チャートの表示オプションを調整します。"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "詳細についてはログを確認してください。"
|
||||
msgid "Check your notification service"
|
||||
msgstr "通知サービスを確認してください"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "クリックしてコピー"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "クリップボードにコピーされました"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "docker compose をコピー"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "docker run をコピー"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "環境変数をコピー"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "ホストをコピー"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linuxコマンドをコピー"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Linuxコマンドをコピー"
|
||||
msgid "Copy text"
|
||||
msgstr "テキストをコピー"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "下記のエージェントのインストールコマンドをコピーするか、<0>ユニバーサルトークン</0>を使用してエージェントを自動登録してください。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "下記のエージェントの<0>docker-compose.yml</0>内容をコピーするか、<1>ユニバーサルトークン</1>を使用してエージェントを自動登録してください。"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "YAMLをコピー"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "デフォルトの期間"
|
||||
msgid "Delete"
|
||||
msgstr "削除"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "フィンガープリントを削除"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "ディスク"
|
||||
@@ -329,6 +351,7 @@ msgstr "パスワードをリセットするためにメールアドレスを入
|
||||
msgid "Enter email address..."
|
||||
msgstr "メールアドレスを入力..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "アラートの更新に失敗しました"
|
||||
msgid "Filter..."
|
||||
msgstr "フィルター..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {分} other {分}}の間"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "グリッド"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew コマンド"
|
||||
@@ -420,6 +448,16 @@ msgstr "無効なメールアドレスです。"
|
||||
msgid "Kernel"
|
||||
msgstr "カーネル"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "言語"
|
||||
@@ -433,6 +471,14 @@ msgstr "レイアウト"
|
||||
msgid "Light"
|
||||
msgstr "ライト"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "ログアウト"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "再起動のたびに、データベース内のシステムはファイルに定義されたシステムに一致するように更新されます。"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "メニューを開く"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "パスワードをリセット"
|
||||
msgid "Resume"
|
||||
msgstr "再開"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "トークンをローテート"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Enterキーまたはカンマを使用してアドレスを保存します。空白のままにするとメール通知が無効になります。"
|
||||
@@ -669,7 +721,6 @@ msgstr "送信"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "システムを表示する際のチャートのデフォルトの時間範囲を設定します。"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "スワップ使用量"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "システム"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "テスト<0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "テスト通知が送信されました"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "接続するにはエージェントがシステム上で実行されている必要があります。以下のエージェントのインストールコマンドをコピーしてください。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "接続するにはエージェントがシステム上で実行されている必要があります。以下のエージェント用<0>docker-compose.yml</0>をコピーしてください。"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "その後、バックエンドにログインして、ユーザーテーブルでユーザーアカウントのパスワードをリセットしてください。"
|
||||
@@ -783,6 +827,33 @@ msgstr "グリッドを切り替え"
|
||||
msgid "Toggle theme"
|
||||
msgstr "テーマを切り替え"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "トークン"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "トークンとフィンガープリント"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "トークンはエージェントの接続と登録を可能にします。フィンガープリントは各システム固有の安定した識別子で、初回接続時に設定されます。"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "センサーがしきい値を超えたときにトリガーされます"
|
||||
@@ -807,6 +878,10 @@ msgstr "ステータスが上から下に切り替わるときにトリガーさ
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "ディスクの使用量がしきい値を超えたときにトリガーされます"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "ユニバーサルトークン"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -856,13 +931,18 @@ msgstr "表示するのに十分なレコードを待っています"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
|
||||
msgstr "翻訳をさらに良くするためにご協力いただけますか?詳細については<0>Crowdin</0>をご覧ください。"
|
||||
msgstr "翻訳をさらに良くするためにご協力をお願いします。詳細については<0>Crowdin</0>をご覧ください。"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / プッシュ通知"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "有効にすると、このトークンはエージェントが事前のシステム作成なしに自己登録することを可能にします。1時間後またはハブの再起動時に期限切れになります。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows コマンド"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML設定"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "ユーザー設定が更新されました。"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ko\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-07 10:06\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Korean\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30일"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "작업"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "URL 추가"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "차트 표시 옵션 변경."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -112,7 +110,7 @@ msgstr "평균"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "컨테이너의 평균 CPU 사용량"
|
||||
msgstr "Docker 컨테이너의 평균 CPU 사용량"
|
||||
|
||||
#. placeholder {0}: data.alert.unit
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
@@ -186,7 +184,7 @@ msgstr "자세한 내용은 로그를 확인하세요."
|
||||
msgid "Check your notification service"
|
||||
msgstr "알림 서비스를 확인하세요."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "클릭하여 복사"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "클립보드에 복사됨"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "docker compose 복사"
|
||||
msgstr "docker compose 내용 복사"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "docker run 복사"
|
||||
msgstr "docker run 명령어 복사"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "호스트 복사"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "리눅스 명령어 복사"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "리눅스 명령어 복사"
|
||||
msgid "Copy text"
|
||||
msgstr "텍스트 복사"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "기본 기간"
|
||||
msgid "Delete"
|
||||
msgstr "삭제"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "디스크"
|
||||
@@ -329,6 +351,7 @@ msgstr "비밀번호를 재설정하려면 이메일 주소를 입력하세요"
|
||||
msgid "Enter email address..."
|
||||
msgstr "이메일 주소 입력..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "알림 수정 실패"
|
||||
msgid "Filter..."
|
||||
msgstr "필터..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {분} other {분}} 동안"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "그리드"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew 명령어"
|
||||
@@ -420,6 +448,16 @@ msgstr "잘못된 이메일 주소입니다."
|
||||
msgid "Kernel"
|
||||
msgstr "커널"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "언어"
|
||||
@@ -433,6 +471,14 @@ msgstr "레이아웃"
|
||||
msgid "Light"
|
||||
msgstr "밝게"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "로그아웃"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "매 시작 시, 데이터베이스가 파일에 정의된 시스템과 일치하도록 업데이트됩니다."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "메뉴 열기"
|
||||
|
||||
@@ -563,7 +611,7 @@ msgstr "일시 중지"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused"
|
||||
msgstr "일시정지됨"
|
||||
msgstr "일시 정지됨"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -635,6 +683,10 @@ msgstr "비밀번호 재설정"
|
||||
msgid "Resume"
|
||||
msgstr "재개"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Enter 키 또는 쉼표를 사용하여 주소를 저장하세요. 이메일 알림을 비활성화하려면 비워 두세요."
|
||||
@@ -669,7 +721,6 @@ msgstr "보냄"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "시스템을 볼 때 차트의 기본 시간 범위를 설정합니다."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "스왑 사용량"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "시스템"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "테스트 <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "테스트 알림이 전송되었습니다."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "에이전트가 시스템에서 실행 중이어야 연결할 수 있습니다. 아래의 에이전트 설치 명령을 복사하세요."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "에이전트가 시스템에서 실행 중이어야 연결할 수 있습니다. 아래의 <0>docker-compose.yml</0>을 복사하세요."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "그런 다음 백엔드에 로그인하여 사용자 테이블에서 사용자 계정 비밀번호를 재설정하세요."
|
||||
@@ -783,6 +827,33 @@ msgstr "그리드 전환"
|
||||
msgid "Toggle theme"
|
||||
msgstr "테마 전환"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "센서가 임계값을 초과할 때 트리거됩니다."
|
||||
@@ -807,6 +878,10 @@ msgstr "시스템의 전원이 켜지거나 꺼질때 트리거됩니다."
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "디스크 사용량이 임계값을 초과할 때 트리거됩니다."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -856,13 +931,18 @@ msgstr "표시할 충분한 기록을 기다리는 중"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
|
||||
msgstr "번역을 더 좋게 만드는 데 도움을 주시겠습니까? 자세한 내용은 <0>Crowdin</0>을 확인하세요."
|
||||
msgstr "번역을 개선하는데 도움을 주시겠습니까? 자세한 내용은 <0>Crowdin</0>을 확인해 주세요."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / 푸시 알림"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows 명령어"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML 구성"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "사용자 설정이 업데이트되었습니다."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: nl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dagen"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Acties"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Voeg URL toe"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Weergaveopties voor grafieken aanpassen."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Controleer de logs voor meer details."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Controleer je meldingsservice"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Klik om te kopiëren"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Gekopieerd naar het klembord"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose kopiëren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run kopiëren"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Env kopiëren"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopieer host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopieer Linux-opdracht"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopieer Linux-opdracht"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopieer tekst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Kopieer de installatie opdracht voor de agent hieronder, of registreer agenten automatisch met een <0>universele token</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Kopieer de<0>docker-compose.yml</0> inhoud voor de agent hieronder, of registreer agenten automatisch met een <1>universele token</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "YAML kopiëren"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Standaard tijdsduur"
|
||||
msgid "Delete"
|
||||
msgstr "Verwijderen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Vingerafdruk verwijderen"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Schijf"
|
||||
@@ -329,6 +351,7 @@ msgstr "Voer een e-mailadres in om het wachtwoord opnieuw in te stellen"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Voer een e-mailadres in..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Bijwerken waarschuwing mislukt"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Vingerafdruk"
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Voor <0>{min}</0> {min, plural, one {minuut} other {minuten}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Raster"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-commando"
|
||||
@@ -420,6 +448,16 @@ msgstr "Ongeldig e-mailadres."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr "L15"
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr "L5"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Taal"
|
||||
@@ -433,6 +471,14 @@ msgstr "Indeling"
|
||||
msgid "Light"
|
||||
msgstr "Licht"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr "Gemiddelde Belasting 15m"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr "Gemiddelde Belasting 5m"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Afmelden"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Bij elke herstart zullen systemen in de database worden bijgewerkt om overeen te komen met de systemen die in het bestand zijn gedefinieerd."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Open menu"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Wachtwoord resetten"
|
||||
msgid "Resume"
|
||||
msgstr "Hervatten"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Roteer Token"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Bewaar het adres met de enter-toets of komma. Laat leeg om e-mailmeldingen uit te schakelen."
|
||||
@@ -669,7 +721,6 @@ msgstr "Verzonden"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Stelt het standaard tijdsbereik voor grafieken in wanneer een systeem wordt bekeken."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap gebruik"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Systeem"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testmelding verzonden"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "De agent moet op het systeem draaien om te verbinden. Kopieer het installatiecommando voor de agent hieronder."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "De agent moet op het systeem draaien om te verbinden. Kopieer de<0>docker-compose.yml</0> voor de agent hieronder."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Log vervolgens in op de backend en reset het wachtwoord van je gebruikersaccount in het gebruikersoverzicht."
|
||||
@@ -783,6 +827,33 @@ msgstr "Schakel raster"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Schakel thema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Tokens & Vingerafdrukken"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Tokens staan agenten toe om verbinding te maken met en te registreren. Vingerafdrukken zijn stabiele Ids deze zijn uniek voor elk systeem, ingesteld bij eerste verbinding."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr "Triggert wanneer de 15 minuten gemiddelde belasting een drempelwaarde overschrijdt"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr "Triggert wanneer de 5 minuten gemiddelde belasting een drempelwaarde overschrijdt"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Triggert wanneer een sensor een drempelwaarde overschrijdt"
|
||||
@@ -807,6 +878,10 @@ msgstr "Triggert wanneer de status schakelt tussen up en down"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrijdt"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Universele token"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Wil je ons helpen onze vertalingen nog beter te maken? Bekijk <0>Crowdin
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Pushmeldingen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Wanneer ingeschakeld kunnen agenten zich met dit token registreren zonder dat er vooraf een systeem aangemaakt hoeft te worden. Het token verloopt na één uur of bij herstart van de hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-commando"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML Configuratie"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: no\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Norwegian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dager"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Handlinger"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Legg Til URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Juster visningsalternativer for diagrammer."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Sjekk loggene for flere detaljer."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Sjekk din meldingstjeneste"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Klikk for å kopiere"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopiert til utklippstavlen"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopier docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopier docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopier vert"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopier Linux-kommando"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopier Linux-kommando"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopier tekst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Standard tidsperiode"
|
||||
msgid "Delete"
|
||||
msgstr "Slett"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -329,6 +351,7 @@ msgstr "Skriv inn e-postadresse for å nullstille passordet"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Skriv inn e-postadresse..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Kunne ikke oppdatere alarm"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "I <0>{min}</0> {min, plural, one {minutt} other {minutter}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Rutenett"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-kommando"
|
||||
@@ -420,6 +448,16 @@ msgstr "Ugyldig e-postadresse."
|
||||
msgid "Kernel"
|
||||
msgstr "Kjerne"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Språk"
|
||||
@@ -433,6 +471,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Lyst"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Logg Ut"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Ved hver omstart vil systemer i databasen bli oppdatert til å matche systemene definert i fila."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Åpne meny"
|
||||
|
||||
@@ -563,7 +611,7 @@ msgstr "Pause"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused"
|
||||
msgstr "Pauset"
|
||||
msgstr "Satt på Pause"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -635,6 +683,10 @@ msgstr "Nullstill Passord"
|
||||
msgid "Resume"
|
||||
msgstr "Gjenoppta"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Lagre adressen med Enter-tasten eller komma. La feltet være tomt for å deaktivere e-postvarsler."
|
||||
@@ -669,7 +721,6 @@ msgstr "Sendt"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Angir standard tidsperiode for diagrammer når et system vises."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap-bruk"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Test-varsling sendt"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Agenten må kjøre på systemet du vil koble til. Kopier installasjons-kommandoen for agenten under."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Agenten må kjøre på systemet du vil koble til. Kopier <0>docker-compose.yml</0> for agenten under."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Logg deretter inn i backend og nullstill passordet på din konto i users-tabellen."
|
||||
@@ -783,6 +827,33 @@ msgstr "Rutenett av/på"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Tema av/på"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Slår inn når enhver sensor overstiger en grenseverdi"
|
||||
@@ -807,6 +878,10 @@ msgstr "Slår inn når statusen veksler mellom oppe og nede"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grenseverdi"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Vil du hjelpe oss med å gjøre oversettelsene enda bedre? Ta en titt p
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push-varslinger"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-kommando"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML Konfigurasjon"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dni"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Akcje"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Dodaj URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Dostosuj opcje wyświetlania wykresów."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Sprawdź logi, aby uzyskać więcej informacji."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Sprawdź swój serwis powiadomień"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Kliknij, aby skopiować"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Skopiowano do schowka"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Skopiuj docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Skopiuj docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopiuj host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiuj polecenie Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopiuj polecenie Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopiuj tekst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Procesor"
|
||||
@@ -266,6 +284,10 @@ msgstr "Domyślny przedział czasu"
|
||||
msgid "Delete"
|
||||
msgstr "Usuń"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Dysk"
|
||||
@@ -305,12 +327,12 @@ msgstr "Dokumentacja"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
msgstr "Nie działa"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Edytuj"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -329,6 +351,7 @@ msgstr "Wprowadź adres e-mail, aby zresetować hasło"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Wprowadź adres e-mail..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Nie udało się zaktualizować powiadomienia"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtruj..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Na <0>{min}</0> {min, plural, one {minutę} other {minut}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Siatka"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Polecenie Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Nieprawidłowy adres e-mail."
|
||||
msgid "Kernel"
|
||||
msgstr "Jądro"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Język"
|
||||
@@ -433,6 +471,14 @@ msgstr "Układ"
|
||||
msgid "Light"
|
||||
msgstr "Jasny"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Wyloguj"
|
||||
@@ -461,7 +507,7 @@ msgstr "Zarządzaj preferencjami wyświetlania i powiadomień."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
msgstr "Instrukcja ręcznej konfiguracji"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Przy każdym ponownym uruchomieniu systemy w bazie danych będą aktualizowane, aby odpowiadały systemom zdefiniowanym w pliku."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Otwórz menu"
|
||||
|
||||
@@ -551,7 +599,7 @@ msgstr "Hasło musi mieć co najmniej 8 znaków."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be less than 72 bytes."
|
||||
msgstr ""
|
||||
msgstr "Hasło musi być mniejsze niż 72 bajty."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Password reset request received"
|
||||
@@ -635,6 +683,10 @@ msgstr "Resetuj hasło"
|
||||
msgid "Resume"
|
||||
msgstr "Wznów"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Zapisz adres, używając klawisza enter lub przecinka. Pozostaw puste, aby wyłączyć powiadomienia e-mail."
|
||||
@@ -646,7 +698,7 @@ msgstr "Zapisz ustawienia"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr ""
|
||||
msgstr "Zapisz system"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -669,7 +721,6 @@ msgstr "Wysłane"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Ustawia domyślny zakres czasowy dla wykresów, gdy system jest wyświetlony."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Użycie pamięci wymiany"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -727,7 +779,7 @@ msgstr "Tabela"
|
||||
#. Temperature label in systems table
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
msgstr "Temperatura"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testowe powiadomienie wysłane."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Agent musi być uruchomiony na systemie, aby nawiązać połączenie. Skopiuj poniżej polecenie instalacji agenta."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Agent musi być uruchomiony na systemie, aby nawiązać połączenie. Skopiuj poniżej plik <0>docker-compose.yml</0> dla agenta."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Następnie zaloguj się do panelu administracyjnego i zresetuj hasło do konta użytkownika w tabeli użytkowników."
|
||||
@@ -783,6 +827,33 @@ msgstr "Przełącz siatkę"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Zmień motyw"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Wyzwalane, gdy jakikolwiek czujnik przekroczy ustalony próg."
|
||||
@@ -807,11 +878,15 @@ msgstr "Wyzwalane, gdy status przełącza się między stanem aktywnym a nieakty
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Wyzwalane, gdy wykorzystanie któregokolwiek dysku przekroczy ustalony próg"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
msgstr "Działa"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Updated in real time. Click on a system to view information."
|
||||
@@ -862,7 +937,12 @@ msgstr "Chcesz pomóc nam uczynić nasze tłumaczenia jeszcze lepszymi? Sprawdź
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Powiadomienia push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Polecenie Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "Konfiguracja YAML"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pt\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dias"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Ações"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Adicionar URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Ajustar opções de exibição para gráficos."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Verifique os logs para mais detalhes."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Verifique seu serviço de notificação"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Clique para copiar"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copiado para a área de transferência"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copiar docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copiar docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Copiar variáveis de ambiente"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Copiar host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copiar comando Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Copiar comando Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Copiar texto"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Copie o comando de instalação do agente abaixo, ou registre agentes automaticamente com um <0>token universal</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Copie o conteúdo do <0>docker-compose.yml</0> do agente abaixo, ou registre agentes automaticamente com um <1>token universal</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copiar YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Período de tempo padrão"
|
||||
msgid "Delete"
|
||||
msgstr "Excluir"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Excluir impressão digital"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disco"
|
||||
@@ -305,7 +327,7 @@ msgstr "Documentação"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
msgstr "“Desligado”"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
@@ -329,6 +351,7 @@ msgstr "Digite o endereço de email para redefinir a senha"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Digite o endereço de email..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Falha ao atualizar alerta"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrar..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Grade"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Comando Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Endereço de email inválido."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
@@ -433,6 +471,14 @@ msgstr "Aspeto"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Sair"
|
||||
@@ -461,7 +507,7 @@ msgstr "Gerenciar preferências de exibição e notificação."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
msgstr "Instruções de configuração manual"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "A cada reinício, os sistemas no banco de dados serão atualizados para corresponder aos sistemas definidos no arquivo."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Abrir menu"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Redefinir Senha"
|
||||
msgid "Resume"
|
||||
msgstr "Retomar"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Rotacionar token"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Salve o endereço usando a tecla enter ou vírgula. Deixe em branco para desativar notificações por email."
|
||||
@@ -669,7 +721,6 @@ msgstr "Enviado"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Define o intervalo de tempo padrão para gráficos quando um sistema é visualizado."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Uso de Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Testar <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Notificação de teste enviada"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "O agente deve estar em execução no sistema para conectar. Copie o comando de instalação para o agente abaixo."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "O agente deve estar em execução no sistema para conectar. Copie o <0>docker-compose.yml</0> para o agente abaixo."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Em seguida, faça login no backend e redefina a senha da sua conta de usuário na tabela de usuários."
|
||||
@@ -783,6 +827,33 @@ msgstr "Alternar grade"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Alternar tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Tokens e Impressões Digitais"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Os tokens permitem que os agentes se conectem e registrem. As impressões digitais são identificadores estáveis únicos para cada sistema, definidos na primeira conexão."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens e impressões digitais são usados para autenticar conexões WebSocket ao hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Dispara quando qualquer sensor excede um limite"
|
||||
@@ -807,11 +878,15 @@ msgstr "Dispara quando o status alterna entre ativo e inativo"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Dispara quando o uso de qualquer disco excede um limite"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Token universal"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
msgstr "“Ligado”"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Updated in real time. Click on a system to view information."
|
||||
@@ -862,7 +937,12 @@ msgstr "Quer nos ajudar a melhorar ainda mais nossas traduções? Confira <0>Cro
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Notificações Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Quando habilitado, este token permite que os agentes se registrem automaticamente sem criação prévia do sistema. Expira após uma hora ou na reinicialização do hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Comando Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "Configuração YAML"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "As configurações do seu usuário foram atualizadas."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ru\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-21 19:36\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 дней"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Действия"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Добавить URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Настроить параметры отображения для графиков."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Проверьте журналы для получения более
|
||||
msgid "Check your notification service"
|
||||
msgstr "Проверьте ваш сервис уведомлений"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Нажмите, чтобы скопировать"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Скопировано в буфер обмена"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Скопировать docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Скопировать docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Скопировать env"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Копировать хост"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Копировать команду Linux"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Копировать команду Linux"
|
||||
msgid "Copy text"
|
||||
msgstr "Копировать текст"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Скопируйте команду для установки агента ниже, или зарегистрируйте агентов автоматически с помощью <0>универсального токена</0>."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Скопируйте содержимое <0>docker-compose.yml</0> для агента ниже, или зарегистрируйте агентов автоматически с помощью <1>универсального токена</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "Скопировать YAML"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Период по умолчанию"
|
||||
msgid "Delete"
|
||||
msgstr "Удалить"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Удалить отпечаток"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Диск"
|
||||
@@ -329,6 +351,7 @@ msgstr "Введите адрес электронной почты для сб
|
||||
msgid "Enter email address..."
|
||||
msgstr "Введите адрес электронной почты..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Не удалось обновить оповещение"
|
||||
msgid "Filter..."
|
||||
msgstr "Фильтр..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Отпечаток"
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "На <0>{min}</0> {min, plural, one {минуту} other {минут}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Сетка"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Команда Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Неверный адрес электронной почты."
|
||||
msgid "Kernel"
|
||||
msgstr "Ядро"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr "Н15"
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr "Н5"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Язык"
|
||||
@@ -433,6 +471,14 @@ msgstr "Макет"
|
||||
msgid "Light"
|
||||
msgstr "Светлая"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr "Средняя загрузка за 15м"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr "Средняя загрузка за 5м"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Выйти"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "При каждом перезапуске системы в базе данных будут обновлены в соответствии с системами, определенными в файле."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Открыть меню"
|
||||
|
||||
@@ -563,7 +611,7 @@ msgstr "Пауза"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused"
|
||||
msgstr "Приостановлено"
|
||||
msgstr "Пауза"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -635,6 +683,10 @@ msgstr "Сбросить пароль"
|
||||
msgid "Resume"
|
||||
msgstr "Возобновить"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Обновить токен"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Сохраните адрес, используя клавишу ввода или запятую. Оставьте пустым, чтобы отключить уведомления по электронной почте."
|
||||
@@ -669,7 +721,6 @@ msgstr "Отправлено"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Устанавливает диапазон времени по умолчанию для графиков при просмотре системы."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Использование подкачки"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Тест <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Тестовое уведомление отправлено"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Агент должен работать на системе для подключения. Скопируйте команду установки агента ниже."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Агент должен работать на системе для подключения. Скопируйте <0>docker-compose.yml</0> для агента ниже."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Затем войдите в бэкенд и сбросьте пароль вашей учетной записи в таблице пользователей."
|
||||
@@ -783,6 +827,33 @@ msgstr "Переключить сетку"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Переключить тему"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Токен"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Токены и отпечатки"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Токены позволяют агентам подключаться и регистрироваться. Отпечатки являются постоянными идентификаторами, уникальными для каждой системы, которые создаются при первом подключении."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Токены и отпечатки используются для аутентификации соединений WebSocket с хабом."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr "Срабатывает, когда средняя загрузка за 15 минут превышает порог"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr "Срабатывает, когда средняя загрузка за 5 минут превышает порог"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Срабатывает, когда любой датчик превышает порог"
|
||||
@@ -807,6 +878,10 @@ msgstr "Срабатывает, когда статус переключаетс
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Срабатывает, когда использование любого диска превышает порог"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Универсальный токен"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Хотите помочь нам улучшить наши перево
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push уведомления"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Если включено, этот токен позволяет агентам регистрироваться самостоятельно без предварительного создания системы. Истекает через час или при перезапуске хаба."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Команда Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML конфигурация"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваши настройки пользователя были обновлены."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: sl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dni"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Dejanja"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Dodaj URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Prilagodi možnosti prikaza za grafikone."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Za več podrobnosti preverite dnevnike."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Preverite storitev obveščanja"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Klikni za kopiranje"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopirano v odložišče"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiraj docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiraj docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopiraj gostitelja"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiraj Linux ukaz"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopiraj Linux ukaz"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopiraj besedilo"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Privzeto časovno obdobje"
|
||||
msgid "Delete"
|
||||
msgstr "Izbriši"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -329,6 +351,7 @@ msgstr "Vnesite e-poštni naslov za ponastavitev gesla"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Vnesite e-poštni naslov..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Opozorila ni bilo mogoče posodobiti"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minuto} other {minut}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Mreža"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Ukaz Homebrew"
|
||||
@@ -420,6 +448,16 @@ msgstr "Napačen e-poštni naslov."
|
||||
msgid "Kernel"
|
||||
msgstr "Jedro"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jezik"
|
||||
@@ -433,6 +471,14 @@ msgstr "Postavitev"
|
||||
msgid "Light"
|
||||
msgstr "Svetlo"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Odjava"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Ob vsakem ponovnem zagonu bodo sistemi v zbirki podatkov posodobljeni, da se bodo ujemali s sistemi, definiranimi v datoteki."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Odpri menu"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Ponastavi geslo"
|
||||
msgid "Resume"
|
||||
msgstr "Nadaljuj"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Shranite naslov s tipko enter ali vejico. Pustite prazno, da onemogočite e-poštna obvestila."
|
||||
@@ -669,7 +721,6 @@ msgstr "Poslano"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Nastavi privzeti časovni obseg za grafikone, ko si ogledujete sistem."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap uporaba"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistemsko"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Preveri <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testno obvestilo je poslano"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Za vzpostavitev povezave mora biti agent zagnan v sistemu. Kopirajte spodnji namestitveni ukaz za agenta."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Za vzpostavitev povezave mora biti agent zagnan v sistemu. Kopirajte <0>docker-compose.yml</0> za spodnjega agenta."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Nato se prijavite v zaledni sistem in ponastavite geslo svojega uporabniškega računa v tabeli uporabnikov."
|
||||
@@ -783,6 +827,33 @@ msgstr "Preklopi način mreže"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Obrni temo"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Sproži se, ko kateri koli senzor preseže prag"
|
||||
@@ -807,6 +878,10 @@ msgstr "Sproži se, ko se stanje preklaplja med gor in dol"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Sproži se, ko uporaba katerega koli diska preseže prag"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Ali nam želite pomagati, da bomo naše prevode še izboljšali? Za več
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / potisna obvestila"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Ukaz Windows"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML nastavitev"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: sv\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 dagar"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Åtgärder"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "Lägg till URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Justera visningsalternativ för diagram."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Kontrollera loggarna för mer information."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Kontrollera din aviseringstjänst"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Klicka för att kopiera"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopierat till urklipp"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiera docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiera docker run"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopiera värd"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiera Linux-kommando"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Kopiera Linux-kommando"
|
||||
msgid "Copy text"
|
||||
msgstr "Kopiera text"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Standardtidsperiod"
|
||||
msgid "Delete"
|
||||
msgstr "Ta bort"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -329,6 +351,7 @@ msgstr "Ange e-postadress för att återställa lösenord"
|
||||
msgid "Enter email address..."
|
||||
msgstr "Ange e-postadress..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Kunde inte uppdatera larm"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrera..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Under <0>{min}</0> {min, plural, one {minut} other {minuter}}"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Rutnät"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-kommando"
|
||||
@@ -420,6 +448,16 @@ msgstr "Ogiltig e-postadress."
|
||||
msgid "Kernel"
|
||||
msgstr "Kärna"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Språk"
|
||||
@@ -433,6 +471,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Ljust"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Logga ut"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Vid varje omstart kommer systemen i databasen att uppdateras för att matcha systemen som definieras i filen."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Öppna menyn"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Återställ lösenord"
|
||||
msgid "Resume"
|
||||
msgstr "Återuppta"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Spara adressen med Enter-tangenten eller komma. Lämna tomt för att inaktivera e-postaviseringar."
|
||||
@@ -669,7 +721,6 @@ msgstr "Skickat"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Anger standardtidsintervallet för diagram när ett system visas."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Swap-användning"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Testa <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Testavisering skickad"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Agenten måste köras på systemet för att ansluta. Kopiera installationskommandot för agenten nedan."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Agenten måste köras på systemet för att ansluta. Kopiera <0>docker-compose.yml</0> för agenten nedan."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Logga sedan in på backend och återställ ditt användarkontos lösenord i användartabellen."
|
||||
@@ -783,6 +827,33 @@ msgstr "Växla rutnät"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Växla tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Utlöses när någon sensor överskrider ett tröskelvärde"
|
||||
@@ -807,6 +878,10 @@ msgstr "Utlöses när status växlar mellan upp och ner"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Utlöses när användningen av någon disk överskrider ett tröskelvärde"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Vill du hjälpa oss att göra våra översättningar ännu bättre? Koll
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push-aviseringar"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-kommando"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML-konfiguration"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dina användarinställningar har uppdaterats."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: tr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 07:27\n"
|
||||
"PO-Revision-Date: 2025-07-13 02:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -49,6 +49,7 @@ msgstr "30 gün"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Eylemler"
|
||||
|
||||
@@ -76,9 +77,6 @@ msgstr "URL Ekle"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Grafikler için görüntüleme seçeneklerini ayarlayın."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
@@ -186,7 +184,7 @@ msgstr "Daha fazla ayrıntı için günlükleri kontrol edin."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Bildirim hizmetinizi kontrol edin"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Kopyalamak için tıklayın"
|
||||
|
||||
@@ -213,20 +211,28 @@ msgid "Copied to clipboard"
|
||||
msgstr "Panoya kopyalandı"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose kopyala"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run kopyala"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr "Ortam değişkenlerini kopyala"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Ana bilgisayarı kopyala"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linux komutunu kopyala"
|
||||
|
||||
@@ -234,6 +240,18 @@ msgstr "Linux komutunu kopyala"
|
||||
msgid "Copy text"
|
||||
msgstr "Metni kopyala"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "Aşağıdaki agent için kurulum komutunu kopyalayın veya <0>evrensel token</0> ile agentları otomatik olarak kaydedin."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Aşağıdaki agent için <0>docker-compose.yml</0> içeriğini kopyalayın veya <1>evrensel token</1> ile agentları otomatik olarak kaydedin."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "YAML'ı kopyala"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
@@ -266,6 +284,10 @@ msgstr "Varsayılan zaman dilimi"
|
||||
msgid "Delete"
|
||||
msgstr "Sil"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Parmak izini sil"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
@@ -329,6 +351,7 @@ msgstr "Şifreyi sıfırlamak için e-posta adresini girin"
|
||||
msgid "Enter email address..."
|
||||
msgstr "E-posta adresini girin..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
@@ -376,6 +399,10 @@ msgstr "Uyarı güncellenemedi"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrele..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {dakika} other {dakika}} için"
|
||||
@@ -399,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Izgara"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew komutu"
|
||||
@@ -420,6 +448,16 @@ msgstr "Geçersiz e-posta adresi."
|
||||
msgid "Kernel"
|
||||
msgstr "Çekirdek"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Dil"
|
||||
@@ -433,6 +471,14 @@ msgstr "Düzen"
|
||||
msgid "Light"
|
||||
msgstr "Açık"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Çıkış Yap"
|
||||
@@ -521,6 +567,8 @@ msgid "On each restart, systems in the database will be updated to match the sys
|
||||
msgstr "Her yeniden başlatmada, veritabanındaki sistemler dosyada tanımlanan sistemlerle eşleşecek şekilde güncellenecektir."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Menüyü aç"
|
||||
|
||||
@@ -635,6 +683,10 @@ msgstr "Şifreyi Sıfırla"
|
||||
msgid "Resume"
|
||||
msgstr "Devam et"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "Token'ı döndür"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Adresleri enter tuşu veya virgül ile kaydedin. E-posta bildirimlerini devre dışı bırakmak için boş bırakın."
|
||||
@@ -669,7 +721,6 @@ msgstr "Gönderildi"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Bir sistem görüntülendiğinde grafikler için varsayılan zaman aralığını ayarlar."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -709,6 +760,7 @@ msgstr "Takas Kullanımı"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistem"
|
||||
|
||||
@@ -746,14 +798,6 @@ msgstr "Test <0>URL</0>"
|
||||
msgid "Test notification sent"
|
||||
msgstr "Test bildirimi gönderildi"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "Bağlanmak için aracının sistemde çalışıyor olması gerekir. Aşağıdaki aracı kurulum komutunu kopyalayın."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "Bağlanmak için aracının sistemde çalışıyor olması gerekir. Aşağıdaki <0>docker-compose.yml</0> dosyasını kopyalayın."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Ardından arka uca giriş yapın ve kullanıcılar tablosunda kullanıcı hesabı şifrenizi sıfırlayın."
|
||||
@@ -783,6 +827,33 @@ msgstr "Izgarayı değiştir"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Temayı değiştir"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "Token'lar ve Parmak İzleri"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "Token'lar agentların bağlanıp kaydolmasına izin verir. Parmak izleri her sisteme özgü kararlı tanımlayıcılardır ve ilk bağlantıda ayarlanır."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Token'lar ve parmak izleri hub'a WebSocket bağlantılarını doğrulamak için kullanılır."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Herhangi bir sensör bir eşiği aştığında tetiklenir"
|
||||
@@ -807,6 +878,10 @@ msgstr "Durum yukarı ve aşağı arasında değiştiğinde tetiklenir"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Herhangi bir diskin kullanımı bir eşiği aştığında tetiklenir"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "Evrensel token"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -862,7 +937,12 @@ msgstr "Çevirilerimizi daha iyi hale getirmemize yardımcı olmak ister misiniz
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Anlık bildirimler"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Etkinleştirildiğinde, bu token agentların önceden sistem oluşturmadan kendilerini kaydetmelerine izin verir. Bir saat sonra veya hub yeniden başlatıldığında sona erer."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows komutu"
|
||||
@@ -884,3 +964,4 @@ msgstr "YAML Yapılandırması"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Kullanıcı ayarlarınız güncellendi."
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user