mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-11 22:05:33 +00:00
Compare commits
94 Commits
v0.13.1
...
top-proces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbbdd49fc2 | ||
|
|
fc0947aa04 | ||
|
|
1d546a4091 | ||
|
|
f60b3bbbfb | ||
|
|
8e99b9f1ad | ||
|
|
fa5ed2bc11 | ||
|
|
21d961ab97 | ||
|
|
aaa93b84d2 | ||
|
|
6a562ce03b | ||
|
|
3dbc48727e | ||
|
|
85ac2e5e9a | ||
|
|
af6bd4e505 | ||
|
|
e54c4b3499 | ||
|
|
078c88f825 | ||
|
|
85169b6c5e | ||
|
|
d0ff8ee2c0 | ||
|
|
e898768997 | ||
|
|
0f5b504f23 | ||
|
|
365d291393 | ||
|
|
3dbab24c0f | ||
|
|
1f67fb7c8d | ||
|
|
219e09fc78 | ||
|
|
cd9c2bd9ab | ||
|
|
9f969d843c | ||
|
|
b22a6472fc | ||
|
|
d231ace28e | ||
|
|
473cb7f437 | ||
|
|
783ed9f456 | ||
|
|
9a9a89ee50 | ||
|
|
5122d0341d | ||
|
|
81731689da | ||
|
|
b3e9857448 | ||
|
|
2eda9eb0e3 | ||
|
|
82a5df5048 | ||
|
|
f11564a7ac | ||
|
|
9df4d29236 | ||
|
|
1452817423 | ||
|
|
c57e496f5e | ||
|
|
6287f7003c | ||
|
|
37037b1f4e | ||
|
|
7cf123a99e | ||
|
|
97394e775f | ||
|
|
d5c381188b | ||
|
|
b107d12a62 | ||
|
|
e646f2c1fc | ||
|
|
b18528d24a | ||
|
|
a6e64df399 | ||
|
|
66ba21dd41 | ||
|
|
1851e7a111 | ||
|
|
74b78e96b3 | ||
|
|
a9657f9c00 | ||
|
|
1dee63a0eb | ||
|
|
d608cf0955 | ||
|
|
b9139a1f9b | ||
|
|
7f372c46db | ||
|
|
40010ad9b9 | ||
|
|
5927f45a4a | ||
|
|
962613df7c | ||
|
|
92b1f236e3 | ||
|
|
a911670a2d | ||
|
|
b0cb0c2269 | ||
|
|
735d03577f | ||
|
|
a33f88d822 | ||
|
|
dfd1fc8fda | ||
|
|
1df08801a2 | ||
|
|
62f5f986bb | ||
|
|
a87b9af9d5 | ||
|
|
03900e54cc | ||
|
|
f4abbd1a5b | ||
|
|
77ed90cb4a | ||
|
|
2fe3b1adb1 | ||
|
|
f56093d0f0 | ||
|
|
77dba42f17 | ||
|
|
e233a0b0dc | ||
|
|
18e4c88875 | ||
|
|
904a6038cd | ||
|
|
ae55b86493 | ||
|
|
5360f762e4 | ||
|
|
0d464787f2 | ||
|
|
24f72ef596 | ||
|
|
2d8739052b | ||
|
|
1e32d13650 | ||
|
|
dbf3f94247 | ||
|
|
8a81c7bbac | ||
|
|
d24150c78b | ||
|
|
013da18789 | ||
|
|
5b663621e4 | ||
|
|
4056345216 | ||
|
|
d00c0488c3 | ||
|
|
d352ce00fa | ||
|
|
1623f5e751 | ||
|
|
612ad1238f | ||
|
|
1ad4409609 | ||
|
|
2a94e1d1ec |
99
.github/workflows/docker-images.yml
vendored
99
.github/workflows/docker-images.yml
vendored
@@ -12,65 +12,137 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# henrygd/beszel
|
||||
- image: henrygd/beszel
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_hub
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# henrygd/beszel-agent
|
||||
- image: henrygd/beszel-agent
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# henrygd/beszel-agent-nvidia
|
||||
- image: henrygd/beszel-agent-nvidia
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||
platforms: linux/amd64
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# henrygd/beszel-agent-intel
|
||||
- image: henrygd/beszel-agent-intel
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent_intel
|
||||
platforms: linux/amd64
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# henrygd/beszel-agent:alpine
|
||||
- image: henrygd/beszel-agent
|
||||
dockerfile: ./internal/dockerfile_agent_alpine
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=alpine
|
||||
type=semver,pattern={{version}}-alpine
|
||||
type=semver,pattern={{major}}.{{minor}}-alpine
|
||||
type=semver,pattern={{major}}-alpine
|
||||
|
||||
# ghcr.io/henrygd/beszel
|
||||
- image: ghcr.io/${{ github.repository }}/beszel
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_hub
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# ghcr.io/henrygd/beszel-agent
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# ghcr.io/henrygd/beszel-agent-nvidia
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||
platforms: linux/amd64
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# ghcr.io/henrygd/beszel-agent-intel
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-intel
|
||||
context: ./
|
||||
dockerfile: ./internal/dockerfile_agent_intel
|
||||
platforms: linux/amd64
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
|
||||
# ghcr.io/henrygd/beszel-agent:alpine
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||
dockerfile: ./internal/dockerfile_agent_alpine
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
tags: |
|
||||
type=raw,value=alpine
|
||||
type=semver,pattern={{version}}-alpine
|
||||
type=semver,pattern={{major}}.{{minor}}-alpine
|
||||
type=semver,pattern={{major}}-alpine
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -100,12 +172,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
tags: |
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||
tags: ${{ matrix.tags }}
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to Docker Hub
|
||||
@@ -123,7 +190,7 @@ jobs:
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: "${{ matrix.context }}"
|
||||
context: ./
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
|
||||
|
||||
@@ -42,6 +42,7 @@ type Agent struct {
|
||||
server *ssh.Server // SSH server
|
||||
dataDir string // Directory for persisting data
|
||||
keys []gossh.PublicKey // SSH public keys
|
||||
smartManager *SmartManager // Manages SMART data
|
||||
}
|
||||
|
||||
// NewAgent creates a new agent with the given data directory for persisting data.
|
||||
@@ -100,11 +101,15 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
||||
// initialize docker manager
|
||||
agent.dockerManager = newDockerManager(agent)
|
||||
|
||||
agent.smartManager, err = NewSmartManager()
|
||||
if err != nil {
|
||||
slog.Debug("SMART", "err", err)
|
||||
}
|
||||
|
||||
// initialize GPU manager
|
||||
if gm, err := NewGPUManager(); err != nil {
|
||||
agent.gpuManager, err = NewGPUManager()
|
||||
if err != nil {
|
||||
slog.Debug("GPU", "err", err)
|
||||
} else {
|
||||
agent.gpuManager = gm
|
||||
}
|
||||
|
||||
// if debugging, print stats
|
||||
@@ -152,7 +157,12 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
||||
data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||
for name, stats := range a.fsStats {
|
||||
if !stats.Root && stats.DiskTotal > 0 {
|
||||
data.Stats.ExtraFs[name] = stats
|
||||
// Use custom name if available, otherwise use device name
|
||||
key := name
|
||||
if stats.Name != "" {
|
||||
key = stats.Name
|
||||
}
|
||||
data.Stats.ExtraFs[key] = stats
|
||||
}
|
||||
}
|
||||
slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"github.com/distatus/battery"
|
||||
)
|
||||
|
||||
var systemHasBattery = false
|
||||
var haveCheckedBattery = false
|
||||
var (
|
||||
systemHasBattery = false
|
||||
haveCheckedBattery = false
|
||||
)
|
||||
|
||||
// HasReadableBattery checks if the system has a battery and returns true if it does.
|
||||
func HasReadableBattery() bool {
|
||||
@@ -19,8 +21,13 @@ func HasReadableBattery() bool {
|
||||
return systemHasBattery
|
||||
}
|
||||
haveCheckedBattery = true
|
||||
bat, err := battery.Get(0)
|
||||
systemHasBattery = err == nil && bat != nil && bat.Design != 0 && bat.Full != 0
|
||||
batteries, err := battery.GetAll()
|
||||
for _, bat := range batteries {
|
||||
if bat != nil && (bat.Full > 0 || bat.Design > 0) {
|
||||
systemHasBattery = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !systemHasBattery {
|
||||
slog.Debug("No battery found", "err", err)
|
||||
}
|
||||
@@ -28,24 +35,44 @@ func HasReadableBattery() bool {
|
||||
}
|
||||
|
||||
// GetBatteryStats returns the current battery percent and charge state
|
||||
// percent = (current charge of all batteries) / (sum of designed/full capacity of all batteries)
|
||||
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
|
||||
if !systemHasBattery {
|
||||
if !HasReadableBattery() {
|
||||
return batteryPercent, batteryState, errors.ErrUnsupported
|
||||
}
|
||||
batteries, err := battery.GetAll()
|
||||
if err != nil || len(batteries) == 0 {
|
||||
return batteryPercent, batteryState, err
|
||||
// we'll handle errors later by skipping batteries with errors, rather
|
||||
// than skipping everything because of the presence of some errors.
|
||||
if len(batteries) == 0 {
|
||||
return batteryPercent, batteryState, errors.New("no batteries")
|
||||
}
|
||||
|
||||
totalCapacity := float64(0)
|
||||
totalCharge := float64(0)
|
||||
for _, bat := range batteries {
|
||||
if bat.Design != 0 {
|
||||
totalCapacity += bat.Design
|
||||
} else {
|
||||
totalCapacity += bat.Full
|
||||
errs, partialErrs := err.(battery.Errors)
|
||||
|
||||
for i, bat := range batteries {
|
||||
if partialErrs && errs[i] != nil {
|
||||
// if there were some errors, like missing data, skip it
|
||||
continue
|
||||
}
|
||||
if bat == nil || bat.Full == 0 {
|
||||
// skip batteries with no capacity. Charge is unlikely to ever be zero, but
|
||||
// we can't guarantee that, so don't skip based on charge.
|
||||
continue
|
||||
}
|
||||
totalCapacity += bat.Full
|
||||
totalCharge += bat.Current
|
||||
}
|
||||
|
||||
if totalCapacity == 0 {
|
||||
// for macs there's sometimes a ghost battery with 0 capacity
|
||||
// https://github.com/distatus/battery/issues/34
|
||||
// Instead of skipping over those batteries, we'll check for total 0 capacity
|
||||
// and return an error. This also prevents a divide by zero.
|
||||
return batteryPercent, batteryState, errors.New("no battery capacity")
|
||||
}
|
||||
|
||||
batteryPercent = uint8(totalCharge / totalCapacity * 100)
|
||||
batteryState = uint8(batteries[0].State.Raw)
|
||||
return batteryPercent, batteryState, nil
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
@@ -143,7 +144,9 @@ func (client *WebSocketClient) OnOpen(conn *gws.Conn) {
|
||||
// OnClose handles WebSocket connection closure.
|
||||
// It logs the closure reason and notifies the connection manager.
|
||||
func (client *WebSocketClient) OnClose(conn *gws.Conn, err error) {
|
||||
slog.Warn("Connection closed", "err", strings.TrimPrefix(err.Error(), "gws: "))
|
||||
if err != nil {
|
||||
slog.Warn("Connection closed", "err", strings.TrimPrefix(err.Error(), "gws: "))
|
||||
}
|
||||
client.agent.connectionManager.eventChan <- WebSocketDisconnect
|
||||
}
|
||||
|
||||
@@ -246,7 +249,13 @@ func (client *WebSocketClient) sendMessage(data any) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.Conn.WriteMessage(gws.OpcodeBinary, bytes)
|
||||
err = client.Conn.WriteMessage(gws.OpcodeBinary, bytes)
|
||||
if err != nil {
|
||||
// If writing fails (e.g., broken pipe due to network issues),
|
||||
// close the connection to trigger reconnection logic (#1263)
|
||||
client.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// sendResponse sends a response with optional request ID for the new protocol
|
||||
@@ -263,6 +272,10 @@ func (client *WebSocketClient) sendResponse(data any, requestID *uint32) error {
|
||||
response.SystemData = v
|
||||
case *common.FingerprintResponse:
|
||||
response.Fingerprint = v
|
||||
case string:
|
||||
response.String = &v
|
||||
case map[string]smart.SmartData:
|
||||
response.SmartData = v
|
||||
// case []byte:
|
||||
// response.RawBytes = v
|
||||
// case string:
|
||||
|
||||
211
agent/cpu.go
211
agent/cpu.go
@@ -2,12 +2,19 @@ package agent
|
||||
|
||||
import (
|
||||
"math"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
var lastCpuTimes = make(map[uint16]cpu.TimesStat)
|
||||
var lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)
|
||||
var lastProcessCpuTimes = make(map[uint16]map[int32]float64)
|
||||
var lastProcessCpuSampleTime = make(map[uint16]time.Time)
|
||||
|
||||
// init initializes the CPU monitoring by storing the initial CPU times
|
||||
// for the default 60-second cache interval.
|
||||
@@ -15,23 +22,206 @@ func init() {
|
||||
if times, err := cpu.Times(false); err == nil {
|
||||
lastCpuTimes[60000] = times[0]
|
||||
}
|
||||
if perCoreTimes, err := cpu.Times(true); err == nil {
|
||||
lastPerCoreCpuTimes[60000] = perCoreTimes
|
||||
}
|
||||
if processes, err := process.Processes(); err == nil {
|
||||
snapshot := make(map[int32]float64, len(processes))
|
||||
for _, proc := range processes {
|
||||
if times, err := proc.Times(); err == nil {
|
||||
snapshot[proc.Pid] = times.Total()
|
||||
}
|
||||
}
|
||||
lastProcessCpuTimes[60000] = snapshot
|
||||
lastProcessCpuSampleTime[60000] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// getCpuPercent calculates the CPU usage percentage using cached previous measurements.
|
||||
// It uses the specified cache time interval to determine the time window for calculation.
|
||||
// Returns the CPU usage percentage (0-100) and any error encountered.
|
||||
func getCpuPercent(cacheTimeMs uint16) (float64, error) {
|
||||
// CpuMetrics contains detailed CPU usage breakdown
|
||||
type CpuMetrics struct {
|
||||
Total float64
|
||||
User float64
|
||||
System float64
|
||||
Iowait float64
|
||||
Steal float64
|
||||
Idle float64
|
||||
}
|
||||
|
||||
// getCpuMetrics calculates detailed CPU usage metrics using cached previous measurements.
|
||||
// It returns percentages for total, user, system, iowait, and steal time.
|
||||
func getCpuMetrics(cacheTimeMs uint16) (CpuMetrics, error) {
|
||||
times, err := cpu.Times(false)
|
||||
if err != nil || len(times) == 0 {
|
||||
return 0, err
|
||||
return CpuMetrics{}, err
|
||||
}
|
||||
// if cacheTimeMs is not in lastCpuTimes, use 60000 as fallback lastCpuTime
|
||||
if _, ok := lastCpuTimes[cacheTimeMs]; !ok {
|
||||
lastCpuTimes[cacheTimeMs] = lastCpuTimes[60000]
|
||||
}
|
||||
delta := calculateBusy(lastCpuTimes[cacheTimeMs], times[0])
|
||||
|
||||
t1 := lastCpuTimes[cacheTimeMs]
|
||||
t2 := times[0]
|
||||
|
||||
t1All, _ := getAllBusy(t1)
|
||||
t2All, _ := getAllBusy(t2)
|
||||
|
||||
totalDelta := t2All - t1All
|
||||
if totalDelta <= 0 {
|
||||
return CpuMetrics{}, nil
|
||||
}
|
||||
|
||||
metrics := CpuMetrics{
|
||||
Total: calculateBusy(t1, t2),
|
||||
User: clampPercent((t2.User - t1.User) / totalDelta * 100),
|
||||
System: clampPercent((t2.System - t1.System) / totalDelta * 100),
|
||||
Iowait: clampPercent((t2.Iowait - t1.Iowait) / totalDelta * 100),
|
||||
Steal: clampPercent((t2.Steal - t1.Steal) / totalDelta * 100),
|
||||
Idle: clampPercent((t2.Idle - t1.Idle) / totalDelta * 100),
|
||||
}
|
||||
|
||||
lastCpuTimes[cacheTimeMs] = times[0]
|
||||
return delta, nil
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// clampPercent ensures the percentage is between 0 and 100
|
||||
func clampPercent(value float64) float64 {
|
||||
return math.Min(100, math.Max(0, value))
|
||||
}
|
||||
|
||||
// getPerCoreCpuUsage calculates per-core CPU busy usage as integer percentages (0-100).
|
||||
// It uses cached previous measurements for the provided cache interval.
|
||||
func getPerCoreCpuUsage(cacheTimeMs uint16) (system.Uint8Slice, error) {
|
||||
perCoreTimes, err := cpu.Times(true)
|
||||
if err != nil || len(perCoreTimes) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize cache if needed
|
||||
if _, ok := lastPerCoreCpuTimes[cacheTimeMs]; !ok {
|
||||
lastPerCoreCpuTimes[cacheTimeMs] = lastPerCoreCpuTimes[60000]
|
||||
}
|
||||
|
||||
lastTimes := lastPerCoreCpuTimes[cacheTimeMs]
|
||||
|
||||
// Limit to the number of cores available in both samples
|
||||
length := len(perCoreTimes)
|
||||
if len(lastTimes) < length {
|
||||
length = len(lastTimes)
|
||||
}
|
||||
|
||||
usage := make([]uint8, length)
|
||||
for i := 0; i < length; i++ {
|
||||
t1 := lastTimes[i]
|
||||
t2 := perCoreTimes[i]
|
||||
usage[i] = uint8(math.Round(calculateBusy(t1, t2)))
|
||||
}
|
||||
|
||||
lastPerCoreCpuTimes[cacheTimeMs] = perCoreTimes
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// getTopCpuProcess returns the process with the highest CPU usage since the last run
|
||||
// for the given cache interval. It returns nil if insufficient data is available.
|
||||
func getTopCpuProcess(cacheTimeMs uint16) (*system.TopCpuProcess, error) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
lastTimes, ok := lastProcessCpuTimes[cacheTimeMs]
|
||||
if !ok {
|
||||
if fallback := lastProcessCpuTimes[60000]; fallback != nil {
|
||||
copied := make(map[int32]float64, len(fallback))
|
||||
for pid, total := range fallback {
|
||||
copied[pid] = total
|
||||
}
|
||||
lastTimes = copied
|
||||
lastProcessCpuTimes[cacheTimeMs] = copied
|
||||
} else {
|
||||
lastTimes = make(map[int32]float64)
|
||||
lastProcessCpuTimes[cacheTimeMs] = lastTimes
|
||||
}
|
||||
}
|
||||
|
||||
lastSample := lastProcessCpuSampleTime[cacheTimeMs]
|
||||
if lastSample.IsZero() {
|
||||
if fallback := lastProcessCpuSampleTime[60000]; !fallback.IsZero() {
|
||||
lastSample = fallback
|
||||
lastProcessCpuSampleTime[cacheTimeMs] = fallback
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := now.Sub(lastSample).Seconds()
|
||||
if lastSample.IsZero() || elapsed <= 0 {
|
||||
snapshot := make(map[int32]float64, len(processes))
|
||||
for _, proc := range processes {
|
||||
if times, err := proc.Times(); err == nil {
|
||||
snapshot[proc.Pid] = times.Total()
|
||||
}
|
||||
}
|
||||
lastProcessCpuTimes[cacheTimeMs] = snapshot
|
||||
lastProcessCpuSampleTime[cacheTimeMs] = now
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cpuCount := float64(runtime.NumCPU())
|
||||
if cpuCount <= 0 {
|
||||
cpuCount = 1
|
||||
}
|
||||
|
||||
snapshot := make(map[int32]float64, len(processes))
|
||||
var topName string
|
||||
var topPercent float64
|
||||
|
||||
for _, proc := range processes {
|
||||
times, err := proc.Times()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
total := times.Total()
|
||||
pid := proc.Pid
|
||||
snapshot[pid] = total
|
||||
|
||||
lastTotal, ok := lastTimes[pid]
|
||||
if !ok || total <= lastTotal {
|
||||
continue
|
||||
}
|
||||
|
||||
percent := clampPercent((total - lastTotal) / (elapsed * cpuCount) * 100)
|
||||
if percent <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
name, err := proc.Name()
|
||||
if err != nil || name == "" {
|
||||
if exe, exeErr := proc.Exe(); exeErr == nil && exe != "" {
|
||||
name = filepath.Base(exe)
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if percent > topPercent {
|
||||
topPercent = percent
|
||||
topName = name
|
||||
}
|
||||
}
|
||||
|
||||
lastProcessCpuTimes[cacheTimeMs] = snapshot
|
||||
lastProcessCpuSampleTime[cacheTimeMs] = now
|
||||
|
||||
if topName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &system.TopCpuProcess{
|
||||
Name: topName,
|
||||
Percent: topPercent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateBusy calculates the CPU busy percentage between two time points.
|
||||
@@ -41,13 +231,10 @@ func calculateBusy(t1, t2 cpu.TimesStat) float64 {
|
||||
t1All, t1Busy := getAllBusy(t1)
|
||||
t2All, t2Busy := getAllBusy(t2)
|
||||
|
||||
if t2Busy <= t1Busy {
|
||||
if t2All <= t1All || t2Busy <= t1Busy {
|
||||
return 0
|
||||
}
|
||||
if t2All <= t1All {
|
||||
return 100
|
||||
}
|
||||
return math.Min(100, math.Max(0, (t2Busy-t1Busy)/(t2All-t1All)*100))
|
||||
return clampPercent((t2Busy - t1Busy) / (t2All - t1All) * 100)
|
||||
}
|
||||
|
||||
// getAllBusy calculates the total CPU time and busy CPU time from CPU times statistics.
|
||||
|
||||
@@ -37,6 +37,16 @@ func (t *DeltaTracker[K, V]) Set(id K, value V) {
|
||||
t.current[id] = value
|
||||
}
|
||||
|
||||
// Snapshot returns a copy of the current map.
|
||||
// func (t *DeltaTracker[K, V]) Snapshot() map[K]V {
|
||||
// t.RLock()
|
||||
// defer t.RUnlock()
|
||||
|
||||
// copyMap := make(map[K]V, len(t.current))
|
||||
// maps.Copy(copyMap, t.current)
|
||||
// return copyMap
|
||||
// }
|
||||
|
||||
// Deltas returns a map of all calculated deltas for the current interval.
|
||||
func (t *DeltaTracker[K, V]) Deltas() map[K]V {
|
||||
t.RLock()
|
||||
@@ -53,6 +63,15 @@ func (t *DeltaTracker[K, V]) Deltas() map[K]V {
|
||||
return deltas
|
||||
}
|
||||
|
||||
// Previous returns the previously recorded value for the given key, if it exists.
|
||||
func (t *DeltaTracker[K, V]) Previous(id K) (V, bool) {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
|
||||
value, ok := t.previous[id]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Delta returns the delta for a single key.
|
||||
// Returns 0 if the key doesn't exist or has no previous value.
|
||||
func (t *DeltaTracker[K, V]) Delta(id K) V {
|
||||
|
||||
@@ -13,11 +13,25 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
// parseFilesystemEntry parses a filesystem entry in the format "device__customname"
|
||||
// Returns the device/filesystem part and the custom name part
|
||||
func parseFilesystemEntry(entry string) (device, customName string) {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if parts := strings.SplitN(entry, "__", 2); len(parts) == 2 {
|
||||
device = strings.TrimSpace(parts[0])
|
||||
customName = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
device = entry
|
||||
}
|
||||
return device, customName
|
||||
}
|
||||
|
||||
// Sets up the filesystems to monitor for disk usage and I/O.
|
||||
func (a *Agent) initializeDiskInfo() {
|
||||
filesystem, _ := GetEnv("FILESYSTEM")
|
||||
efPath := "/extra-filesystems"
|
||||
hasRoot := false
|
||||
isWindows := runtime.GOOS == "windows"
|
||||
|
||||
partitions, err := disk.Partitions(false)
|
||||
if err != nil {
|
||||
@@ -25,6 +39,13 @@ func (a *Agent) initializeDiskInfo() {
|
||||
}
|
||||
slog.Debug("Disk", "partitions", partitions)
|
||||
|
||||
// trim trailing backslash for Windows devices (#1361)
|
||||
if isWindows {
|
||||
for i, p := range partitions {
|
||||
partitions[i].Device = strings.TrimSuffix(p.Device, "\\")
|
||||
}
|
||||
}
|
||||
|
||||
// ioContext := context.WithValue(a.sensorsContext,
|
||||
// common.EnvKey, common.EnvMap{common.HostProcEnvKey: "/tmp/testproc"},
|
||||
// )
|
||||
@@ -37,9 +58,9 @@ func (a *Agent) initializeDiskInfo() {
|
||||
slog.Debug("Disk I/O", "diskstats", diskIoCounters)
|
||||
|
||||
// Helper function to add a filesystem to fsStats if it doesn't exist
|
||||
addFsStat := func(device, mountpoint string, root bool) {
|
||||
addFsStat := func(device, mountpoint string, root bool, customName ...string) {
|
||||
var key string
|
||||
if runtime.GOOS == "windows" {
|
||||
if isWindows {
|
||||
key = device
|
||||
} else {
|
||||
key = filepath.Base(device)
|
||||
@@ -66,7 +87,11 @@ func (a *Agent) initializeDiskInfo() {
|
||||
}
|
||||
}
|
||||
}
|
||||
a.fsStats[key] = &system.FsStats{Root: root, Mountpoint: mountpoint}
|
||||
fsStats := &system.FsStats{Root: root, Mountpoint: mountpoint}
|
||||
if len(customName) > 0 && customName[0] != "" {
|
||||
fsStats.Name = customName[0]
|
||||
}
|
||||
a.fsStats[key] = fsStats
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +111,14 @@ func (a *Agent) initializeDiskInfo() {
|
||||
|
||||
// Add EXTRA_FILESYSTEMS env var values to fsStats
|
||||
if extraFilesystems, exists := GetEnv("EXTRA_FILESYSTEMS"); exists {
|
||||
for _, fs := range strings.Split(extraFilesystems, ",") {
|
||||
for _, fsEntry := range strings.Split(extraFilesystems, ",") {
|
||||
// Parse custom name from format: device__customname
|
||||
fs, customName := parseFilesystemEntry(fsEntry)
|
||||
|
||||
found := false
|
||||
for _, p := range partitions {
|
||||
if strings.HasSuffix(p.Device, fs) || p.Mountpoint == fs {
|
||||
addFsStat(p.Device, p.Mountpoint, false)
|
||||
addFsStat(p.Device, p.Mountpoint, false, customName)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -98,7 +126,7 @@ func (a *Agent) initializeDiskInfo() {
|
||||
// if not in partitions, test if we can get disk usage
|
||||
if !found {
|
||||
if _, err := disk.Usage(fs); err == nil {
|
||||
addFsStat(filepath.Base(fs), fs, false)
|
||||
addFsStat(filepath.Base(fs), fs, false, customName)
|
||||
} else {
|
||||
slog.Error("Invalid filesystem", "name", fs, "err", err)
|
||||
}
|
||||
@@ -120,7 +148,8 @@ func (a *Agent) initializeDiskInfo() {
|
||||
|
||||
// Check if device is in /extra-filesystems
|
||||
if strings.HasPrefix(p.Mountpoint, efPath) {
|
||||
addFsStat(p.Device, p.Mountpoint, false)
|
||||
device, customName := parseFilesystemEntry(p.Mountpoint)
|
||||
addFsStat(device, p.Mountpoint, false, customName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +164,8 @@ func (a *Agent) initializeDiskInfo() {
|
||||
mountpoint := filepath.Join(efPath, folder.Name())
|
||||
slog.Debug("/extra-filesystems", "mountpoint", mountpoint)
|
||||
if !existingMountpoints[mountpoint] {
|
||||
addFsStat(folder.Name(), mountpoint, false)
|
||||
device, customName := parseFilesystemEntry(folder.Name())
|
||||
addFsStat(device, mountpoint, false, customName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
235
agent/disk_test.go
Normal file
235
agent/disk_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseFilesystemEntry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedFs string
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
name: "simple device name",
|
||||
input: "sda1",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "device with custom name",
|
||||
input: "sda1__my-storage",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "my-storage",
|
||||
},
|
||||
{
|
||||
name: "full device path with custom name",
|
||||
input: "/dev/sdb1__backup-drive",
|
||||
expectedFs: "/dev/sdb1",
|
||||
expectedName: "backup-drive",
|
||||
},
|
||||
{
|
||||
name: "NVMe device with custom name",
|
||||
input: "nvme0n1p2__fast-ssd",
|
||||
expectedFs: "nvme0n1p2",
|
||||
expectedName: "fast-ssd",
|
||||
},
|
||||
{
|
||||
name: "whitespace trimmed",
|
||||
input: " sda2__trimmed-name ",
|
||||
expectedFs: "sda2",
|
||||
expectedName: "trimmed-name",
|
||||
},
|
||||
{
|
||||
name: "empty custom name",
|
||||
input: "sda3__",
|
||||
expectedFs: "sda3",
|
||||
expectedName: "",
|
||||
},
|
||||
{
|
||||
name: "empty device name",
|
||||
input: "__just-custom",
|
||||
expectedFs: "",
|
||||
expectedName: "just-custom",
|
||||
},
|
||||
{
|
||||
name: "multiple underscores in custom name",
|
||||
input: "sda1__my_custom_drive",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "my_custom_drive",
|
||||
},
|
||||
{
|
||||
name: "custom name with spaces",
|
||||
input: "sda1__My Storage Drive",
|
||||
expectedFs: "sda1",
|
||||
expectedName: "My Storage Drive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fsEntry := strings.TrimSpace(tt.input)
|
||||
var fs, customName string
|
||||
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
|
||||
fs = strings.TrimSpace(parts[0])
|
||||
customName = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
fs = fsEntry
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedFs, fs)
|
||||
assert.Equal(t, tt.expectedName, customName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
|
||||
// Set up environment variables
|
||||
oldEnv := os.Getenv("EXTRA_FILESYSTEMS")
|
||||
defer func() {
|
||||
if oldEnv != "" {
|
||||
os.Setenv("EXTRA_FILESYSTEMS", oldEnv)
|
||||
} else {
|
||||
os.Unsetenv("EXTRA_FILESYSTEMS")
|
||||
}
|
||||
}()
|
||||
|
||||
// Test with custom names
|
||||
os.Setenv("EXTRA_FILESYSTEMS", "sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2")
|
||||
|
||||
// Mock disk partitions (we'll just test the parsing logic)
|
||||
// Since the actual disk operations are system-dependent, we'll focus on the parsing
|
||||
testCases := []struct {
|
||||
envValue string
|
||||
expectedFs []string
|
||||
expectedNames map[string]string
|
||||
}{
|
||||
{
|
||||
envValue: "sda1__my-storage,sdb1__backup-drive",
|
||||
expectedFs: []string{"sda1", "sdb1"},
|
||||
expectedNames: map[string]string{
|
||||
"sda1": "my-storage",
|
||||
"sdb1": "backup-drive",
|
||||
},
|
||||
},
|
||||
{
|
||||
envValue: "sda1,nvme0n1p2__fast-ssd",
|
||||
expectedFs: []string{"sda1", "nvme0n1p2"},
|
||||
expectedNames: map[string]string{
|
||||
"nvme0n1p2": "fast-ssd",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("env_"+tc.envValue, func(t *testing.T) {
|
||||
os.Setenv("EXTRA_FILESYSTEMS", tc.envValue)
|
||||
|
||||
// Create mock partitions that would match our test cases
|
||||
partitions := []disk.PartitionStat{}
|
||||
for _, fs := range tc.expectedFs {
|
||||
if strings.HasPrefix(fs, "/dev/") {
|
||||
partitions = append(partitions, disk.PartitionStat{
|
||||
Device: fs,
|
||||
Mountpoint: fs,
|
||||
})
|
||||
} else {
|
||||
partitions = append(partitions, disk.PartitionStat{
|
||||
Device: "/dev/" + fs,
|
||||
Mountpoint: "/" + fs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test the parsing logic by calling the relevant part
|
||||
// We'll create a simplified version to test just the parsing
|
||||
extraFilesystems := tc.envValue
|
||||
for _, fsEntry := range strings.Split(extraFilesystems, ",") {
|
||||
// Parse the entry
|
||||
fsEntry = strings.TrimSpace(fsEntry)
|
||||
var fs, customName string
|
||||
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
|
||||
fs = strings.TrimSpace(parts[0])
|
||||
customName = strings.TrimSpace(parts[1])
|
||||
} else {
|
||||
fs = fsEntry
|
||||
}
|
||||
|
||||
// Verify the device is in our expected list
|
||||
assert.Contains(t, tc.expectedFs, fs, "parsed device should be in expected list")
|
||||
|
||||
// Check if custom name should exist
|
||||
if expectedName, exists := tc.expectedNames[fs]; exists {
|
||||
assert.Equal(t, expectedName, customName, "custom name should match expected")
|
||||
} else {
|
||||
assert.Empty(t, customName, "custom name should be empty when not expected")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsStatsWithCustomNames(t *testing.T) {
|
||||
// Test that FsStats properly stores custom names
|
||||
fsStats := &system.FsStats{
|
||||
Mountpoint: "/mnt/storage",
|
||||
Name: "my-custom-storage",
|
||||
DiskTotal: 100.0,
|
||||
DiskUsed: 50.0,
|
||||
}
|
||||
|
||||
assert.Equal(t, "my-custom-storage", fsStats.Name)
|
||||
assert.Equal(t, "/mnt/storage", fsStats.Mountpoint)
|
||||
assert.Equal(t, 100.0, fsStats.DiskTotal)
|
||||
assert.Equal(t, 50.0, fsStats.DiskUsed)
|
||||
}
|
||||
|
||||
func TestExtraFsKeyGeneration(t *testing.T) {
|
||||
// Test the logic for generating ExtraFs keys with custom names
|
||||
testCases := []struct {
|
||||
name string
|
||||
deviceName string
|
||||
customName string
|
||||
expectedKey string
|
||||
}{
|
||||
{
|
||||
name: "with custom name",
|
||||
deviceName: "sda1",
|
||||
customName: "my-storage",
|
||||
expectedKey: "my-storage",
|
||||
},
|
||||
{
|
||||
name: "without custom name",
|
||||
deviceName: "sda1",
|
||||
customName: "",
|
||||
expectedKey: "sda1",
|
||||
},
|
||||
{
|
||||
name: "empty custom name falls back to device",
|
||||
deviceName: "nvme0n1p2",
|
||||
customName: "",
|
||||
expectedKey: "nvme0n1p2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Simulate the key generation logic from agent.go
|
||||
key := tc.deviceName
|
||||
if tc.customName != "" {
|
||||
key = tc.customName
|
||||
}
|
||||
assert.Equal(t, tc.expectedKey, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
268
agent/docker.go
268
agent/docker.go
@@ -3,13 +3,17 @@ package agent
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -25,6 +29,16 @@ const (
|
||||
dockerTimeoutMs = 2100
|
||||
// Maximum realistic network speed (5 GB/s) to detect bad deltas
|
||||
maxNetworkSpeedBps uint64 = 5e9
|
||||
// Maximum conceivable memory usage of a container (100TB) to detect bad memory stats
|
||||
maxMemoryUsage uint64 = 100 * 1024 * 1024 * 1024 * 1024
|
||||
// Number of log lines to request when fetching container logs
|
||||
dockerLogsTail = 200
|
||||
// Maximum size of a single log frame (1MB) to prevent memory exhaustion
|
||||
// A single log line larger than 1MB is likely an error or misconfiguration
|
||||
maxLogFrameSize = 1024 * 1024
|
||||
// Maximum total log content size (5MB) to prevent memory exhaustion
|
||||
// This provides a reasonable limit for network transfer and browser rendering
|
||||
maxTotalLogSize = 5 * 1024 * 1024
|
||||
)
|
||||
|
||||
type dockerManager struct {
|
||||
@@ -40,6 +54,7 @@ type dockerManager struct {
|
||||
buf *bytes.Buffer // Buffer to store and read response bodies
|
||||
decoder *json.Decoder // Reusable JSON decoder that reads from buf
|
||||
apiStats *container.ApiStats // Reusable API stats object
|
||||
excludeContainers []string // Patterns to exclude containers by name
|
||||
|
||||
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
|
||||
// Maps cache time intervals to container-specific CPU usage tracking
|
||||
@@ -81,6 +96,19 @@ func (d *dockerManager) dequeue() {
|
||||
}
|
||||
}
|
||||
|
||||
// shouldExcludeContainer checks if a container name matches any exclusion pattern
|
||||
func (dm *dockerManager) shouldExcludeContainer(name string) bool {
|
||||
if len(dm.excludeContainers) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, pattern := range dm.excludeContainers {
|
||||
if match, _ := path.Match(pattern, name); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns stats for all running containers with cache-time-aware delta tracking
|
||||
func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats, error) {
|
||||
resp, err := dm.client.Get("http://localhost/containers/json")
|
||||
@@ -108,6 +136,13 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats,
|
||||
|
||||
for _, ctr := range dm.apiContainerList {
|
||||
ctr.IdShort = ctr.Id[:12]
|
||||
|
||||
// Skip this container if it matches the exclusion pattern
|
||||
if dm.shouldExcludeContainer(ctr.Names[0][1:]) {
|
||||
slog.Debug("Excluding container", "name", ctr.Names[0][1:])
|
||||
continue
|
||||
}
|
||||
|
||||
dm.validIds[ctr.IdShort] = struct{}{}
|
||||
// check if container is less than 1 minute old (possible restart)
|
||||
// note: can't use Created field because it's not updated on restart
|
||||
@@ -198,17 +233,17 @@ func calculateMemoryUsage(apiStats *container.ApiStats, isWindows bool) (uint64,
|
||||
return apiStats.MemoryStats.PrivateWorkingSet, nil
|
||||
}
|
||||
|
||||
// Check if container has valid data, otherwise may be in restart loop (#103)
|
||||
if apiStats.MemoryStats.Usage == 0 {
|
||||
return 0, fmt.Errorf("no memory stats available")
|
||||
}
|
||||
|
||||
memCache := apiStats.MemoryStats.Stats.InactiveFile
|
||||
if memCache == 0 {
|
||||
memCache = apiStats.MemoryStats.Stats.Cache
|
||||
}
|
||||
|
||||
return apiStats.MemoryStats.Usage - memCache, nil
|
||||
usedDelta := apiStats.MemoryStats.Usage - memCache
|
||||
if usedDelta <= 0 || usedDelta > maxMemoryUsage {
|
||||
return 0, fmt.Errorf("bad memory stats")
|
||||
}
|
||||
|
||||
return usedDelta, nil
|
||||
}
|
||||
|
||||
// getNetworkTracker returns the DeltaTracker for a specific cache time, creating it if needed
|
||||
@@ -299,11 +334,46 @@ func updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemo
|
||||
stats.PrevReadTime = readTime
|
||||
}
|
||||
|
||||
func parseDockerStatus(status string) (string, container.DockerHealth) {
|
||||
trimmed := strings.TrimSpace(status)
|
||||
if trimmed == "" {
|
||||
return "", container.DockerHealthNone
|
||||
}
|
||||
|
||||
// Remove "About " from status
|
||||
trimmed = strings.Replace(trimmed, "About ", "", 1)
|
||||
|
||||
openIdx := strings.LastIndex(trimmed, "(")
|
||||
if openIdx == -1 || !strings.HasSuffix(trimmed, ")") {
|
||||
return trimmed, container.DockerHealthNone
|
||||
}
|
||||
|
||||
statusText := strings.TrimSpace(trimmed[:openIdx])
|
||||
if statusText == "" {
|
||||
statusText = trimmed
|
||||
}
|
||||
|
||||
healthText := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(trimmed[openIdx+1:], ")")))
|
||||
// Some Docker statuses include a "health:" prefix inside the parentheses.
|
||||
// Strip it so it maps correctly to the known health states.
|
||||
if colonIdx := strings.IndexRune(healthText, ':'); colonIdx != -1 {
|
||||
prefix := strings.TrimSpace(healthText[:colonIdx])
|
||||
if prefix == "health" || prefix == "health status" {
|
||||
healthText = strings.TrimSpace(healthText[colonIdx+1:])
|
||||
}
|
||||
}
|
||||
if health, ok := container.DockerHealthStrings[healthText]; ok {
|
||||
return statusText, health
|
||||
}
|
||||
|
||||
return trimmed, container.DockerHealthNone
|
||||
}
|
||||
|
||||
// Updates stats for individual container with cache-time-aware delta tracking
|
||||
func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeMs uint16) error {
|
||||
name := ctr.Names[0][1:]
|
||||
|
||||
resp, err := dm.client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
||||
resp, err := dm.client.Get(fmt.Sprintf("http://localhost/containers/%s/stats?stream=0&one-shot=1", ctr.IdShort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -314,10 +384,16 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeM
|
||||
// add empty values if they doesn't exist in map
|
||||
stats, initialized := dm.containerStatsMap[ctr.IdShort]
|
||||
if !initialized {
|
||||
stats = &container.Stats{Name: name}
|
||||
stats = &container.Stats{Name: name, Id: ctr.IdShort, Image: ctr.Image}
|
||||
dm.containerStatsMap[ctr.IdShort] = stats
|
||||
}
|
||||
|
||||
stats.Id = ctr.IdShort
|
||||
|
||||
statusText, health := parseDockerStatus(ctr.Status)
|
||||
stats.Status = statusText
|
||||
stats.Health = health
|
||||
|
||||
// reset current stats
|
||||
stats.Cpu = 0
|
||||
stats.Mem = 0
|
||||
@@ -449,6 +525,19 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
userAgent: "Docker-Client/",
|
||||
}
|
||||
|
||||
// Read container exclusion patterns from environment variable
|
||||
var excludeContainers []string
|
||||
if excludeStr, set := GetEnv("EXCLUDE_CONTAINERS"); set && excludeStr != "" {
|
||||
parts := strings.SplitSeq(excludeStr, ",")
|
||||
for part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
excludeContainers = append(excludeContainers, trimmed)
|
||||
}
|
||||
}
|
||||
slog.Info("EXCLUDE_CONTAINERS", "patterns", excludeContainers)
|
||||
}
|
||||
|
||||
manager := &dockerManager{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
@@ -458,6 +547,7 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
sem: make(chan struct{}, 5),
|
||||
apiContainerList: []*container.ApiInfo{},
|
||||
apiStats: &container.ApiStats{},
|
||||
excludeContainers: excludeContainers,
|
||||
|
||||
// Initialize cache-time-aware tracking structures
|
||||
lastCpuContainer: make(map[uint16]map[string]uint64),
|
||||
@@ -474,28 +564,49 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
return manager
|
||||
}
|
||||
|
||||
// Check docker version
|
||||
// (versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch)
|
||||
// this can take up to 5 seconds with retry, so run in goroutine
|
||||
go manager.checkDockerVersion()
|
||||
|
||||
// give version check a chance to complete before returning
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// checkDockerVersion checks Docker version and sets goodDockerVersion if at least 25.0.0.
|
||||
// Versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch.
|
||||
func (dm *dockerManager) checkDockerVersion() {
|
||||
var err error
|
||||
var resp *http.Response
|
||||
var versionInfo struct {
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
resp, err := manager.client.Get("http://localhost/version")
|
||||
const versionMaxTries = 2
|
||||
for i := 1; i <= versionMaxTries; i++ {
|
||||
resp, err = dm.client.Get("http://localhost/version")
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if i < versionMaxTries {
|
||||
slog.Debug("Failed to get Docker version; retrying", "attempt", i, "error", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return manager
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.decode(resp, &versionInfo); err != nil {
|
||||
return manager
|
||||
if err := dm.decode(resp, &versionInfo); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
dm.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
|
||||
}
|
||||
|
||||
// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe.
|
||||
@@ -525,3 +636,122 @@ func getDockerHost() string {
|
||||
}
|
||||
return scheme + socks[0]
|
||||
}
|
||||
|
||||
// getContainerInfo fetches the inspection data for a container
|
||||
func (dm *dockerManager) getContainerInfo(ctx context.Context, containerID string) ([]byte, error) {
|
||||
endpoint := fmt.Sprintf("http://localhost/containers/%s/json", containerID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := dm.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
return nil, fmt.Errorf("container info request failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
// Remove sensitive environment variables from Config.Env
|
||||
var containerInfo map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&containerInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config, ok := containerInfo["Config"].(map[string]any); ok {
|
||||
delete(config, "Env")
|
||||
}
|
||||
|
||||
return json.Marshal(containerInfo)
|
||||
}
|
||||
|
||||
// getLogs fetches the logs for a container
|
||||
func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) {
|
||||
endpoint := fmt.Sprintf("http://localhost/containers/%s/logs?stdout=1&stderr=1&tail=%d", containerID, dockerLogsTail)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := dm.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
return "", fmt.Errorf("logs request failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
if err := decodeDockerLogStream(resp.Body, &builder); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error {
|
||||
const headerSize = 8
|
||||
var header [headerSize]byte
|
||||
buf := make([]byte, 0, dockerLogsTail*200)
|
||||
totalBytesRead := 0
|
||||
|
||||
for {
|
||||
if _, err := io.ReadFull(reader, header[:]); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
frameLen := binary.BigEndian.Uint32(header[4:])
|
||||
if frameLen == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prevent memory exhaustion from excessively large frames
|
||||
if frameLen > maxLogFrameSize {
|
||||
return fmt.Errorf("log frame size (%d) exceeds maximum (%d)", frameLen, maxLogFrameSize)
|
||||
}
|
||||
|
||||
// Check if reading this frame would exceed total log size limit
|
||||
if totalBytesRead+int(frameLen) > maxTotalLogSize {
|
||||
// Read and discard remaining data to avoid blocking
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(reader, int64(frameLen)))
|
||||
slog.Debug("Truncating logs: limit reached", "read", totalBytesRead, "limit", maxTotalLogSize)
|
||||
return nil
|
||||
}
|
||||
|
||||
buf = allocateBuffer(buf, int(frameLen))
|
||||
if _, err := io.ReadFull(reader, buf[:frameLen]); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
if len(buf) > 0 {
|
||||
builder.Write(buf[:min(int(frameLen), len(buf))])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
builder.Write(buf[:frameLen])
|
||||
totalBytesRead += int(frameLen)
|
||||
}
|
||||
}
|
||||
|
||||
func allocateBuffer(current []byte, needed int) []byte {
|
||||
if cap(current) >= needed {
|
||||
return current[:needed]
|
||||
}
|
||||
return make([]byte, needed)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -858,11 +860,61 @@ func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
|
||||
assert.Equal(t, uint64(200000), recvTracker2.Delta(ctr.IdShort))
|
||||
}
|
||||
|
||||
func TestParseDockerStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedStatus string
|
||||
expectedHealth container.DockerHealth
|
||||
}{
|
||||
{
|
||||
name: "status with About an removed",
|
||||
input: "Up About an hour (healthy)",
|
||||
expectedStatus: "Up an hour",
|
||||
expectedHealth: container.DockerHealthHealthy,
|
||||
},
|
||||
{
|
||||
name: "status without About an unchanged",
|
||||
input: "Up 2 hours (healthy)",
|
||||
expectedStatus: "Up 2 hours",
|
||||
expectedHealth: container.DockerHealthHealthy,
|
||||
},
|
||||
{
|
||||
name: "status with About and no parentheses",
|
||||
input: "Up About an hour",
|
||||
expectedStatus: "Up an hour",
|
||||
expectedHealth: container.DockerHealthNone,
|
||||
},
|
||||
{
|
||||
name: "status without parentheses",
|
||||
input: "Created",
|
||||
expectedStatus: "Created",
|
||||
expectedHealth: container.DockerHealthNone,
|
||||
},
|
||||
{
|
||||
name: "empty status",
|
||||
input: "",
|
||||
expectedStatus: "",
|
||||
expectedHealth: container.DockerHealthNone,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
status, health := parseDockerStatus(tt.input)
|
||||
assert.Equal(t, tt.expectedStatus, status)
|
||||
assert.Equal(t, tt.expectedHealth, health)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantsAndUtilityFunctions(t *testing.T) {
|
||||
// Test constants are properly defined
|
||||
assert.Equal(t, uint16(60000), defaultCacheTimeMs)
|
||||
assert.Equal(t, uint64(5e9), maxNetworkSpeedBps)
|
||||
assert.Equal(t, 2100, dockerTimeoutMs)
|
||||
assert.Equal(t, uint32(1024*1024), uint32(maxLogFrameSize)) // 1MB
|
||||
assert.Equal(t, 5*1024*1024, maxTotalLogSize) // 5MB
|
||||
|
||||
// Test utility functions
|
||||
assert.Equal(t, 1.5, twoDecimals(1.499))
|
||||
@@ -873,3 +925,281 @@ func TestConstantsAndUtilityFunctions(t *testing.T) {
|
||||
assert.Equal(t, 0.5, bytesToMegabytes(524288)) // 512 KB
|
||||
assert.Equal(t, 0.0, bytesToMegabytes(0))
|
||||
}
|
||||
|
||||
func TestDecodeDockerLogStream(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple log entry",
|
||||
input: []byte{
|
||||
// Frame 1: stdout, 11 bytes
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,
|
||||
'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd',
|
||||
},
|
||||
expected: "Hello World",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multiple frames",
|
||||
input: []byte{
|
||||
// Frame 1: stdout, 5 bytes
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||
'H', 'e', 'l', 'l', 'o',
|
||||
// Frame 2: stdout, 5 bytes
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||
'W', 'o', 'r', 'l', 'd',
|
||||
},
|
||||
expected: "HelloWorld",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "zero length frame",
|
||||
input: []byte{
|
||||
// Frame 1: stdout, 0 bytes
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Frame 2: stdout, 5 bytes
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||
'H', 'e', 'l', 'l', 'o',
|
||||
},
|
||||
expected: "Hello",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: []byte{},
|
||||
expected: "",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := bytes.NewReader(tt.input)
|
||||
var builder strings.Builder
|
||||
err := decodeDockerLogStream(reader, &builder)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, builder.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {
|
||||
t.Run("excessively large frame should error", func(t *testing.T) {
|
||||
// Create a frame with size exceeding maxLogFrameSize
|
||||
excessiveSize := uint32(maxLogFrameSize + 1)
|
||||
input := []byte{
|
||||
// Frame header with excessive size
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
byte(excessiveSize >> 24), byte(excessiveSize >> 16), byte(excessiveSize >> 8), byte(excessiveSize),
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(input)
|
||||
var builder strings.Builder
|
||||
err := decodeDockerLogStream(reader, &builder)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "log frame size")
|
||||
assert.Contains(t, err.Error(), "exceeds maximum")
|
||||
})
|
||||
|
||||
t.Run("total size limit should truncate", func(t *testing.T) {
|
||||
// Create frames that exceed maxTotalLogSize (5MB)
|
||||
// Use frames within maxLogFrameSize (1MB) to avoid single-frame rejection
|
||||
frameSize := uint32(800 * 1024) // 800KB per frame
|
||||
var input []byte
|
||||
|
||||
// Frames 1-6: 800KB each (total 4.8MB - within 5MB limit)
|
||||
for i := 0; i < 6; i++ {
|
||||
char := byte('A' + i)
|
||||
frameHeader := []byte{
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
byte(frameSize >> 24), byte(frameSize >> 16), byte(frameSize >> 8), byte(frameSize),
|
||||
}
|
||||
input = append(input, frameHeader...)
|
||||
input = append(input, bytes.Repeat([]byte{char}, int(frameSize))...)
|
||||
}
|
||||
|
||||
// Frame 7: 800KB (would bring total to 5.6MB, exceeding 5MB limit - should be truncated)
|
||||
frame7Header := []byte{
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
byte(frameSize >> 24), byte(frameSize >> 16), byte(frameSize >> 8), byte(frameSize),
|
||||
}
|
||||
input = append(input, frame7Header...)
|
||||
input = append(input, bytes.Repeat([]byte{'Z'}, int(frameSize))...)
|
||||
|
||||
reader := bytes.NewReader(input)
|
||||
var builder strings.Builder
|
||||
err := decodeDockerLogStream(reader, &builder)
|
||||
|
||||
// Should complete without error (graceful truncation)
|
||||
assert.NoError(t, err)
|
||||
// Should have read 6 frames (4.8MB total, stopping before 7th would exceed 5MB limit)
|
||||
expectedSize := int(frameSize) * 6
|
||||
assert.Equal(t, expectedSize, builder.Len())
|
||||
// Should contain A-F but not Z
|
||||
result := builder.String()
|
||||
assert.Contains(t, result, "A")
|
||||
assert.Contains(t, result, "F")
|
||||
assert.NotContains(t, result, "Z")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllocateBuffer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currentCap int
|
||||
needed int
|
||||
expectedCap int
|
||||
shouldRealloc bool
|
||||
}{
|
||||
{
|
||||
name: "buffer has enough capacity",
|
||||
currentCap: 1024,
|
||||
needed: 512,
|
||||
expectedCap: 1024,
|
||||
shouldRealloc: false,
|
||||
},
|
||||
{
|
||||
name: "buffer needs reallocation",
|
||||
currentCap: 512,
|
||||
needed: 1024,
|
||||
expectedCap: 1024,
|
||||
shouldRealloc: true,
|
||||
},
|
||||
{
|
||||
name: "buffer needs exact size",
|
||||
currentCap: 1024,
|
||||
needed: 1024,
|
||||
expectedCap: 1024,
|
||||
shouldRealloc: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
current := make([]byte, 0, tt.currentCap)
|
||||
result := allocateBuffer(current, tt.needed)
|
||||
|
||||
assert.Equal(t, tt.needed, len(result))
|
||||
assert.GreaterOrEqual(t, cap(result), tt.expectedCap)
|
||||
|
||||
if tt.shouldRealloc {
|
||||
// If reallocation was needed, capacity should be at least the needed size
|
||||
assert.GreaterOrEqual(t, cap(result), tt.needed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldExcludeContainer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
containerName string
|
||||
patterns []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty patterns excludes nothing",
|
||||
containerName: "any-container",
|
||||
patterns: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "exact match - excluded",
|
||||
containerName: "test-web",
|
||||
patterns: []string{"test-web", "test-api"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "exact match - not excluded",
|
||||
containerName: "prod-web",
|
||||
patterns: []string{"test-web", "test-api"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard prefix match - excluded",
|
||||
containerName: "test-web",
|
||||
patterns: []string{"test-*"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard prefix match - not excluded",
|
||||
containerName: "prod-web",
|
||||
patterns: []string{"test-*"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard suffix match - excluded",
|
||||
containerName: "myapp-staging",
|
||||
patterns: []string{"*-staging"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard suffix match - not excluded",
|
||||
containerName: "myapp-prod",
|
||||
patterns: []string{"*-staging"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard both sides match - excluded",
|
||||
containerName: "test-myapp-staging",
|
||||
patterns: []string{"*-myapp-*"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard both sides match - not excluded",
|
||||
containerName: "prod-yourapp-live",
|
||||
patterns: []string{"*-myapp-*"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns - matches first",
|
||||
containerName: "test-container",
|
||||
patterns: []string{"test-*", "*-staging"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns - matches second",
|
||||
containerName: "myapp-staging",
|
||||
patterns: []string{"test-*", "*-staging"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns - no match",
|
||||
containerName: "prod-web",
|
||||
patterns: []string{"test-*", "*-staging"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "mixed exact and wildcard - exact match",
|
||||
containerName: "temp-container",
|
||||
patterns: []string{"temp-container", "test-*"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "mixed exact and wildcard - wildcard match",
|
||||
containerName: "test-web",
|
||||
patterns: []string{"temp-container", "test-*"},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dm := &dockerManager{
|
||||
excludeContainers: tt.patterns,
|
||||
}
|
||||
result := dm.shouldExcludeContainer(tt.containerName)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,12 @@ func (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {
|
||||
|
||||
// collectIntelStats executes intel_gpu_top in text mode (-l) and parses the output
|
||||
func (gm *GPUManager) collectIntelStats() (err error) {
|
||||
cmd := exec.Command(intelGpuStatsCmd, "-s", intelGpuStatsInterval, "-l")
|
||||
// Build command arguments, optionally selecting a device via -d
|
||||
args := []string{"-s", intelGpuStatsInterval, "-l"}
|
||||
if dev, ok := GetEnv("INTEL_GPU_DEVICE"); ok && dev != "" {
|
||||
args = append(args, "-d", dev)
|
||||
}
|
||||
cmd := exec.Command(intelGpuStatsCmd, args...)
|
||||
// Avoid blocking if intel_gpu_top writes to stderr
|
||||
cmd.Stderr = io.Discard
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -1624,3 +1626,42 @@ func TestParseIntelData(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntelCollectorDeviceEnv(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PATH", dir)
|
||||
|
||||
// Prepare a file to capture args
|
||||
argsFile := filepath.Join(dir, "args.txt")
|
||||
|
||||
// Create a fake intel_gpu_top that records its arguments and prints minimal valid output
|
||||
scriptPath := filepath.Join(dir, "intel_gpu_top")
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
echo "$@" > %s
|
||||
echo "Freq MHz IRQ RC6 Power W IMC MiB/s RCS VCS"
|
||||
echo " req act /s %% gpu pkg rd wr %% se wa %% se wa"
|
||||
echo "226 223 338 58 2.00 2.69 1820 965 0.00 0 0 0.00 0 0"
|
||||
echo "189 187 412 67 1.80 2.45 1950 823 8.50 2 1 15.00 1 0"
|
||||
`, argsFile)
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set device selector via prefixed env var
|
||||
t.Setenv("BESZEL_AGENT_INTEL_GPU_DEVICE", "sriov")
|
||||
|
||||
gm := &GPUManager{GpuDataMap: make(map[string]*system.GPUData)}
|
||||
if err := gm.collectIntelStats(); err != nil {
|
||||
t.Fatalf("collectIntelStats error: %v", err)
|
||||
}
|
||||
|
||||
// Verify that -d sriov was passed
|
||||
data, err := os.ReadFile(argsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed reading args file: %v", err)
|
||||
}
|
||||
argsStr := strings.TrimSpace(string(data))
|
||||
require.Contains(t, argsStr, "-d sriov")
|
||||
require.Contains(t, argsStr, "-s ")
|
||||
require.Contains(t, argsStr, "-l")
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// HandlerContext provides context for request handlers
|
||||
@@ -43,6 +47,9 @@ func NewHandlerRegistry() *HandlerRegistry {
|
||||
|
||||
registry.Register(common.GetData, &GetDataHandler{})
|
||||
registry.Register(common.CheckFingerprint, &CheckFingerprintHandler{})
|
||||
registry.Register(common.GetContainerLogs, &GetContainerLogsHandler{})
|
||||
registry.Register(common.GetContainerInfo, &GetContainerInfoHandler{})
|
||||
registry.Register(common.GetSmartData, &GetSmartDataHandler{})
|
||||
|
||||
return registry
|
||||
}
|
||||
@@ -99,3 +106,71 @@ type CheckFingerprintHandler struct{}
|
||||
func (h *CheckFingerprintHandler) Handle(hctx *HandlerContext) error {
|
||||
return hctx.Client.handleAuthChallenge(hctx.Request, hctx.RequestID)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetContainerLogsHandler handles container log requests
|
||||
type GetContainerLogsHandler struct{}
|
||||
|
||||
func (h *GetContainerLogsHandler) Handle(hctx *HandlerContext) error {
|
||||
if hctx.Agent.dockerManager == nil {
|
||||
return hctx.SendResponse("", hctx.RequestID)
|
||||
}
|
||||
|
||||
var req common.ContainerLogsRequest
|
||||
if err := cbor.Unmarshal(hctx.Request.Data, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
logContent, err := hctx.Agent.dockerManager.getLogs(ctx, req.ContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return hctx.SendResponse(logContent, hctx.RequestID)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetContainerInfoHandler handles container info requests
|
||||
type GetContainerInfoHandler struct{}
|
||||
|
||||
func (h *GetContainerInfoHandler) Handle(hctx *HandlerContext) error {
|
||||
if hctx.Agent.dockerManager == nil {
|
||||
return hctx.SendResponse("", hctx.RequestID)
|
||||
}
|
||||
|
||||
var req common.ContainerInfoRequest
|
||||
if err := cbor.Unmarshal(hctx.Request.Data, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
info, err := hctx.Agent.dockerManager.getContainerInfo(ctx, req.ContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return hctx.SendResponse(string(info), hctx.RequestID)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetSmartDataHandler handles SMART data requests
|
||||
type GetSmartDataHandler struct{}
|
||||
|
||||
func (h *GetSmartDataHandler) Handle(hctx *HandlerContext) error {
|
||||
if hctx.Agent.smartManager == nil {
|
||||
// return empty map to indicate no data
|
||||
return hctx.SendResponse(map[string]smart.SmartData{}, hctx.RequestID)
|
||||
}
|
||||
if err := hctx.Agent.smartManager.Refresh(false); err != nil {
|
||||
slog.Debug("smart refresh failed", "err", err)
|
||||
}
|
||||
data := hctx.Agent.smartManager.GetCurrentData()
|
||||
return hctx.SendResponse(data, hctx.RequestID)
|
||||
}
|
||||
|
||||
@@ -172,8 +172,24 @@ func (a *Agent) sumAndTrackPerNicDeltas(cacheTimeMs uint16, msElapsed uint64, ne
|
||||
tracker.Set(upKey, v.BytesSent)
|
||||
tracker.Set(downKey, v.BytesRecv)
|
||||
if msElapsed > 0 {
|
||||
upDelta = tracker.Delta(upKey) * 1000 / msElapsed
|
||||
downDelta = tracker.Delta(downKey) * 1000 / msElapsed
|
||||
if prevVal, ok := tracker.Previous(upKey); ok {
|
||||
var deltaBytes uint64
|
||||
if v.BytesSent >= prevVal {
|
||||
deltaBytes = v.BytesSent - prevVal
|
||||
} else {
|
||||
deltaBytes = v.BytesSent
|
||||
}
|
||||
upDelta = deltaBytes * 1000 / msElapsed
|
||||
}
|
||||
if prevVal, ok := tracker.Previous(downKey); ok {
|
||||
var deltaBytes uint64
|
||||
if v.BytesRecv >= prevVal {
|
||||
deltaBytes = v.BytesRecv - prevVal
|
||||
} else {
|
||||
deltaBytes = v.BytesRecv
|
||||
}
|
||||
downDelta = deltaBytes * 1000 / msElapsed
|
||||
}
|
||||
}
|
||||
systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv}
|
||||
}
|
||||
@@ -212,6 +228,10 @@ func (a *Agent) applyNetworkTotals(
|
||||
a.initializeNetIoStats()
|
||||
delete(a.netIoStats, cacheTimeMs)
|
||||
delete(a.netInterfaceDeltaTrackers, cacheTimeMs)
|
||||
systemStats.NetworkSent = 0
|
||||
systemStats.NetworkRecv = 0
|
||||
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = 0, 0
|
||||
return
|
||||
}
|
||||
|
||||
systemStats.NetworkSent = networkSentPs
|
||||
|
||||
@@ -338,6 +338,43 @@ func TestSumAndTrackPerNicDeltas(t *testing.T) {
|
||||
assert.Equal(t, uint64(7000), ni[1])
|
||||
}
|
||||
|
||||
func TestSumAndTrackPerNicDeltasHandlesCounterReset(t *testing.T) {
|
||||
a := &Agent{
|
||||
netInterfaces: map[string]struct{}{"eth0": {}},
|
||||
netInterfaceDeltaTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
|
||||
}
|
||||
|
||||
cache := uint16(77)
|
||||
|
||||
// First interval establishes baseline values
|
||||
initial := []psutilNet.IOCountersStat{{Name: "eth0", BytesSent: 4_000, BytesRecv: 6_000}}
|
||||
statsInitial := &system.Stats{}
|
||||
a.ensureNetworkInterfacesMap(statsInitial)
|
||||
_, _ = a.sumAndTrackPerNicDeltas(cache, 0, initial, statsInitial)
|
||||
|
||||
// Second interval increments counters normally so previous snapshot gets populated
|
||||
increment := []psutilNet.IOCountersStat{{Name: "eth0", BytesSent: 9_000, BytesRecv: 11_000}}
|
||||
statsIncrement := &system.Stats{}
|
||||
a.ensureNetworkInterfacesMap(statsIncrement)
|
||||
_, _ = a.sumAndTrackPerNicDeltas(cache, 1_000, increment, statsIncrement)
|
||||
|
||||
niIncrement, ok := statsIncrement.NetworkInterfaces["eth0"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, uint64(5_000), niIncrement[0])
|
||||
assert.Equal(t, uint64(5_000), niIncrement[1])
|
||||
|
||||
// Third interval simulates counter reset (values drop below previous totals)
|
||||
reset := []psutilNet.IOCountersStat{{Name: "eth0", BytesSent: 1_200, BytesRecv: 1_500}}
|
||||
statsReset := &system.Stats{}
|
||||
a.ensureNetworkInterfacesMap(statsReset)
|
||||
_, _ = a.sumAndTrackPerNicDeltas(cache, 1_000, reset, statsReset)
|
||||
|
||||
niReset, ok := statsReset.NetworkInterfaces["eth0"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, uint64(1_200), niReset[0], "upload delta should match new counter value after reset")
|
||||
assert.Equal(t, uint64(1_500), niReset[1], "download delta should match new counter value after reset")
|
||||
}
|
||||
|
||||
func TestApplyNetworkTotals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -441,10 +478,13 @@ func TestApplyNetworkTotals(t *testing.T) {
|
||||
)
|
||||
|
||||
if tt.expectReset {
|
||||
// Should have reset network tracking state - delta trackers should be cleared
|
||||
// Note: initializeNetIoStats resets the maps, then applyNetworkTotals sets nis back
|
||||
assert.Contains(t, a.netIoStats, cacheTimeMs, "cache entry should exist after reset")
|
||||
// Should have reset network tracking state - maps cleared and stats zeroed
|
||||
assert.NotContains(t, a.netIoStats, cacheTimeMs, "cache entry should be cleared after reset")
|
||||
assert.NotContains(t, a.netInterfaceDeltaTrackers, cacheTimeMs, "tracker should be cleared on reset")
|
||||
assert.Zero(t, systemStats.NetworkSent)
|
||||
assert.Zero(t, systemStats.NetworkRecv)
|
||||
assert.Zero(t, systemStats.Bandwidth[0])
|
||||
assert.Zero(t, systemStats.Bandwidth[1])
|
||||
} else {
|
||||
// Should have applied stats
|
||||
assert.Equal(t, tt.expectedNetworkSent, systemStats.NetworkSent)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/blang/semver"
|
||||
@@ -168,6 +169,10 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes
|
||||
switch v := data.(type) {
|
||||
case *system.CombinedData:
|
||||
response.SystemData = v
|
||||
case string:
|
||||
response.String = &v
|
||||
case map[string]smart.SmartData:
|
||||
response.SmartData = v
|
||||
default:
|
||||
response.Error = fmt.Sprintf("unsupported response type: %T", data)
|
||||
}
|
||||
|
||||
897
agent/smart.go
Normal file
897
agent/smart.go
Normal file
@@ -0,0 +1,897 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// SmartManager manages data collection for SMART devices
|
||||
type SmartManager struct {
|
||||
sync.Mutex
|
||||
SmartDataMap map[string]*smart.SmartData
|
||||
SmartDevices []*DeviceInfo
|
||||
refreshMutex sync.Mutex
|
||||
lastScanTime time.Time
|
||||
}
|
||||
|
||||
type scanOutput struct {
|
||||
Devices []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
InfoName string `json:"info_name"`
|
||||
Protocol string `json:"protocol"`
|
||||
} `json:"devices"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
InfoName string `json:"info_name"`
|
||||
Protocol string `json:"protocol"`
|
||||
// typeVerified reports whether we have already parsed SMART data for this device
|
||||
// with the stored parserType. When true we can skip re-running the detection logic.
|
||||
typeVerified bool
|
||||
// parserType holds the parser type (nvme, sat, scsi) that last succeeded.
|
||||
parserType string
|
||||
}
|
||||
|
||||
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
||||
|
||||
// Refresh updates SMART data for all known devices
|
||||
func (sm *SmartManager) Refresh(forceScan bool) error {
|
||||
sm.refreshMutex.Lock()
|
||||
defer sm.refreshMutex.Unlock()
|
||||
|
||||
scanErr := sm.ScanDevices(false)
|
||||
if scanErr != nil {
|
||||
slog.Debug("smartctl scan failed", "err", scanErr)
|
||||
}
|
||||
|
||||
devices := sm.devicesSnapshot()
|
||||
var collectErr error
|
||||
for _, deviceInfo := range devices {
|
||||
if deviceInfo == nil {
|
||||
continue
|
||||
}
|
||||
if err := sm.CollectSmart(deviceInfo); err != nil {
|
||||
slog.Debug("smartctl collect failed", "device", deviceInfo.Name, "err", err)
|
||||
collectErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return sm.resolveRefreshError(scanErr, collectErr)
|
||||
}
|
||||
|
||||
// devicesSnapshot returns a copy of the current device slice to avoid iterating
|
||||
// while holding the primary mutex for longer than necessary.
|
||||
func (sm *SmartManager) devicesSnapshot() []*DeviceInfo {
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
devices := make([]*DeviceInfo, len(sm.SmartDevices))
|
||||
copy(devices, sm.SmartDevices)
|
||||
return devices
|
||||
}
|
||||
|
||||
// hasSmartData reports whether any SMART data has been collected.
|
||||
// func (sm *SmartManager) hasSmartData() bool {
|
||||
// sm.Lock()
|
||||
// defer sm.Unlock()
|
||||
|
||||
// return len(sm.SmartDataMap) > 0
|
||||
// }
|
||||
|
||||
// resolveRefreshError determines the proper error to return after a refresh.
|
||||
func (sm *SmartManager) resolveRefreshError(scanErr, collectErr error) error {
|
||||
sm.Lock()
|
||||
noDevices := len(sm.SmartDevices) == 0
|
||||
noData := len(sm.SmartDataMap) == 0
|
||||
sm.Unlock()
|
||||
|
||||
if noDevices {
|
||||
if scanErr != nil {
|
||||
return scanErr
|
||||
}
|
||||
}
|
||||
|
||||
if !noData {
|
||||
return nil
|
||||
}
|
||||
|
||||
if collectErr != nil {
|
||||
return collectErr
|
||||
}
|
||||
if scanErr != nil {
|
||||
return scanErr
|
||||
}
|
||||
return errNoValidSmartData
|
||||
}
|
||||
|
||||
// GetCurrentData returns the current SMART data
|
||||
func (sm *SmartManager) GetCurrentData() map[string]smart.SmartData {
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
result := make(map[string]smart.SmartData, len(sm.SmartDataMap))
|
||||
for key, value := range sm.SmartDataMap {
|
||||
if value != nil {
|
||||
result[key] = *value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ScanDevices scans for SMART devices
|
||||
// Scan devices using `smartctl --scan -j`
|
||||
// If scan fails, return error
|
||||
// If scan succeeds, parse the output and update the SmartDevices slice
|
||||
func (sm *SmartManager) ScanDevices(force bool) error {
|
||||
if !force && time.Since(sm.lastScanTime) < 30*time.Minute {
|
||||
return nil
|
||||
}
|
||||
sm.lastScanTime = time.Now()
|
||||
currentDevices := sm.devicesSnapshot()
|
||||
|
||||
var configuredDevices []*DeviceInfo
|
||||
if configuredRaw, ok := GetEnv("SMART_DEVICES"); ok {
|
||||
slog.Info("SMART_DEVICES", "value", configuredRaw)
|
||||
config := strings.TrimSpace(configuredRaw)
|
||||
if config == "" {
|
||||
return errNoValidSmartData
|
||||
}
|
||||
|
||||
parsedDevices, err := sm.parseConfiguredDevices(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuredDevices = parsedDevices
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "smartctl", "--scan", "-j")
|
||||
output, err := cmd.Output()
|
||||
|
||||
var (
|
||||
scanErr error
|
||||
scannedDevices []*DeviceInfo
|
||||
hasValidScan bool
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
scanErr = err
|
||||
} else {
|
||||
scannedDevices, hasValidScan = sm.parseScan(output)
|
||||
if !hasValidScan {
|
||||
scanErr = errNoValidSmartData
|
||||
}
|
||||
}
|
||||
|
||||
finalDevices := mergeDeviceLists(currentDevices, scannedDevices, configuredDevices)
|
||||
sm.updateSmartDevices(finalDevices)
|
||||
|
||||
if len(finalDevices) == 0 {
|
||||
if scanErr != nil {
|
||||
slog.Debug("smartctl scan failed", "err", scanErr)
|
||||
return scanErr
|
||||
}
|
||||
return errNoValidSmartData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *SmartManager) parseConfiguredDevices(config string) ([]*DeviceInfo, error) {
|
||||
entries := strings.Split(config, ",")
|
||||
devices := make([]*DeviceInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(entry, ":", 2)
|
||||
|
||||
name := strings.TrimSpace(parts[0])
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("invalid SMART_DEVICES entry %q", entry)
|
||||
}
|
||||
|
||||
devType := ""
|
||||
if len(parts) == 2 {
|
||||
devType = strings.ToLower(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
|
||||
devices = append(devices, &DeviceInfo{
|
||||
Name: name,
|
||||
Type: devType,
|
||||
})
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
return nil, errNoValidSmartData
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// detectSmartOutputType inspects sections that are unique to each smartctl
|
||||
// JSON schema (NVMe, ATA/SATA, SCSI) to determine which parser should be used
|
||||
// when the reported device type is ambiguous or missing.
|
||||
func detectSmartOutputType(output []byte) string {
|
||||
var hints struct {
|
||||
AtaSmartAttributes json.RawMessage `json:"ata_smart_attributes"`
|
||||
NVMeSmartHealthInformationLog json.RawMessage `json:"nvme_smart_health_information_log"`
|
||||
ScsiErrorCounterLog json.RawMessage `json:"scsi_error_counter_log"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(output, &hints); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch {
|
||||
case hasJSONValue(hints.NVMeSmartHealthInformationLog):
|
||||
return "nvme"
|
||||
case hasJSONValue(hints.AtaSmartAttributes):
|
||||
return "sat"
|
||||
case hasJSONValue(hints.ScsiErrorCounterLog):
|
||||
return "scsi"
|
||||
default:
|
||||
return "sat"
|
||||
}
|
||||
}
|
||||
|
||||
// hasJSONValue reports whether a JSON payload contains a concrete value. The
|
||||
// smartctl output often emits "null" for sections that do not apply, so we
|
||||
// only treat non-null content as a hint.
|
||||
func hasJSONValue(raw json.RawMessage) bool {
|
||||
if len(raw) == 0 {
|
||||
return false
|
||||
}
|
||||
trimmed := strings.TrimSpace(string(raw))
|
||||
return trimmed != "" && trimmed != "null"
|
||||
}
|
||||
|
||||
func normalizeParserType(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "nvme", "sntasmedia", "sntrealtek":
|
||||
return "nvme"
|
||||
case "sat", "ata":
|
||||
return "sat"
|
||||
case "scsi":
|
||||
return "scsi"
|
||||
default:
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
|
||||
// parseSmartOutput attempts each SMART parser, optionally detecting the type when
|
||||
// it is not provided, and updates the device info when a parser succeeds.
|
||||
func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte) bool {
|
||||
parsers := []struct {
|
||||
Type string
|
||||
Parse func([]byte) (bool, int)
|
||||
}{
|
||||
{Type: "nvme", Parse: sm.parseSmartForNvme},
|
||||
{Type: "sat", Parse: sm.parseSmartForSata},
|
||||
{Type: "scsi", Parse: sm.parseSmartForScsi},
|
||||
}
|
||||
|
||||
deviceType := normalizeParserType(deviceInfo.parserType)
|
||||
if deviceType == "" {
|
||||
deviceType = normalizeParserType(deviceInfo.Type)
|
||||
}
|
||||
if deviceInfo.parserType == "" {
|
||||
switch deviceType {
|
||||
case "nvme", "sat", "scsi":
|
||||
deviceInfo.parserType = deviceType
|
||||
}
|
||||
}
|
||||
|
||||
// Only run the type detection when we do not yet know which parser works
|
||||
// or the previous attempt failed.
|
||||
needsDetection := deviceType == "" || !deviceInfo.typeVerified
|
||||
if needsDetection {
|
||||
structureType := detectSmartOutputType(output)
|
||||
if deviceType != structureType {
|
||||
deviceType = structureType
|
||||
deviceInfo.parserType = structureType
|
||||
deviceInfo.typeVerified = false
|
||||
}
|
||||
if deviceInfo.Type == "" || strings.EqualFold(deviceInfo.Type, structureType) {
|
||||
deviceInfo.Type = structureType
|
||||
}
|
||||
}
|
||||
|
||||
// Try the most likely parser first, but keep the remaining parsers in reserve
|
||||
// so an incorrect hint never leaves the device unparsed.
|
||||
selectedParsers := make([]struct {
|
||||
Type string
|
||||
Parse func([]byte) (bool, int)
|
||||
}, 0, len(parsers))
|
||||
if deviceType != "" {
|
||||
for _, parser := range parsers {
|
||||
if parser.Type == deviceType {
|
||||
selectedParsers = append(selectedParsers, parser)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, parser := range parsers {
|
||||
alreadySelected := false
|
||||
for _, selected := range selectedParsers {
|
||||
if selected.Type == parser.Type {
|
||||
alreadySelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if alreadySelected {
|
||||
continue
|
||||
}
|
||||
selectedParsers = append(selectedParsers, parser)
|
||||
}
|
||||
|
||||
// Try the selected parsers in order until we find one that succeeds.
|
||||
for _, parser := range selectedParsers {
|
||||
hasData, _ := parser.Parse(output)
|
||||
if hasData {
|
||||
deviceInfo.parserType = parser.Type
|
||||
if deviceInfo.Type == "" || strings.EqualFold(deviceInfo.Type, parser.Type) {
|
||||
deviceInfo.Type = parser.Type
|
||||
}
|
||||
// Remember that this parser is valid so future refreshes can bypass
|
||||
// detection entirely.
|
||||
deviceInfo.typeVerified = true
|
||||
return true
|
||||
}
|
||||
slog.Debug("parser failed", "device", deviceInfo.Name, "parser", parser.Type)
|
||||
}
|
||||
|
||||
// Leave verification false so the next pass will attempt detection again.
|
||||
deviceInfo.typeVerified = false
|
||||
slog.Debug("parsing failed", "device", deviceInfo.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
// CollectSmart collects SMART data for a device
|
||||
// Collect data using `smartctl -d <type> -aj /dev/<device>` when device type is known
|
||||
// Always attempts to parse output even if command fails, as some data may still be available
|
||||
// If collect fails, return error
|
||||
// If collect succeeds, parse the output and update the SmartDataMap
|
||||
// Uses -n standby to avoid waking up sleeping disks, but bypasses standby mode
|
||||
// for initial data collection when no cached data exists
|
||||
func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
||||
// slog.Info("collecting SMART data", "device", deviceInfo.Name, "type", deviceInfo.Type, "has_existing_data", sm.hasDataForDevice(deviceInfo.Name))
|
||||
|
||||
// Check if we have any existing data for this device
|
||||
hasExistingData := sm.hasDataForDevice(deviceInfo.Name)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Try with -n standby first if we have existing data
|
||||
args := sm.smartctlArgs(deviceInfo, true)
|
||||
cmd := exec.CommandContext(ctx, "smartctl", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Check if device is in standby (exit status 2)
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 2 {
|
||||
if hasExistingData {
|
||||
// Device is in standby and we have cached data, keep using cache
|
||||
return nil
|
||||
}
|
||||
// No cached data, need to collect initial data by bypassing standby
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel2()
|
||||
args = sm.smartctlArgs(deviceInfo, false)
|
||||
cmd = exec.CommandContext(ctx2, "smartctl", args...)
|
||||
output, err = cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
hasValidData := sm.parseSmartOutput(deviceInfo, output)
|
||||
|
||||
if !hasValidData {
|
||||
if err != nil {
|
||||
slog.Debug("smartctl failed", "device", deviceInfo.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
slog.Debug("no valid SMART data found", "device", deviceInfo.Name)
|
||||
return errNoValidSmartData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// smartctlArgs returns the arguments for the smartctl command
|
||||
// based on the device type and whether to include standby mode
|
||||
func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool) []string {
|
||||
args := make([]string, 0, 7)
|
||||
|
||||
if deviceInfo != nil {
|
||||
deviceType := strings.ToLower(deviceInfo.Type)
|
||||
// types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345
|
||||
if deviceType != "" && deviceType != "scsi" && deviceType != "ata" {
|
||||
args = append(args, "-d", deviceInfo.Type)
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, "-a", "--json=c")
|
||||
|
||||
if includeStandby {
|
||||
args = append(args, "-n", "standby")
|
||||
}
|
||||
|
||||
if deviceInfo != nil {
|
||||
args = append(args, deviceInfo.Name)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// hasDataForDevice checks if we have cached SMART data for a specific device
|
||||
func (sm *SmartManager) hasDataForDevice(deviceName string) bool {
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
// Check if any cached data has this device name
|
||||
for _, data := range sm.SmartDataMap {
|
||||
if data != nil && data.DiskName == deviceName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseScan parses the output of smartctl --scan -j and returns the discovered devices.
|
||||
func (sm *SmartManager) parseScan(output []byte) ([]*DeviceInfo, bool) {
|
||||
scan := &scanOutput{}
|
||||
|
||||
if err := json.Unmarshal(output, scan); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if len(scan.Devices) == 0 {
|
||||
slog.Debug("no devices found in smartctl scan")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
devices := make([]*DeviceInfo, 0, len(scan.Devices))
|
||||
for _, device := range scan.Devices {
|
||||
slog.Debug("smartctl scan", "name", device.Name, "type", device.Type, "protocol", device.Protocol)
|
||||
devices = append(devices, &DeviceInfo{
|
||||
Name: device.Name,
|
||||
Type: device.Type,
|
||||
InfoName: device.InfoName,
|
||||
Protocol: device.Protocol,
|
||||
})
|
||||
}
|
||||
|
||||
return devices, true
|
||||
}
|
||||
|
||||
// mergeDeviceLists combines scanned and configured SMART devices, preferring
|
||||
// configured SMART_DEVICES when both sources reference the same device.
|
||||
func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo {
|
||||
if len(scanned) == 0 && len(configured) == 0 {
|
||||
return existing
|
||||
}
|
||||
|
||||
// preserveVerifiedType copies the verified type/parser metadata from an existing
|
||||
// device record so that subsequent scans/config updates never downgrade a
|
||||
// previously verified device.
|
||||
preserveVerifiedType := func(target, prev *DeviceInfo) {
|
||||
if prev == nil || !prev.typeVerified {
|
||||
return
|
||||
}
|
||||
target.Type = prev.Type
|
||||
target.typeVerified = true
|
||||
target.parserType = prev.parserType
|
||||
}
|
||||
|
||||
existingIndex := make(map[string]*DeviceInfo, len(existing))
|
||||
for _, dev := range existing {
|
||||
if dev == nil || dev.Name == "" {
|
||||
continue
|
||||
}
|
||||
existingIndex[dev.Name] = dev
|
||||
}
|
||||
|
||||
finalDevices := make([]*DeviceInfo, 0, len(scanned)+len(configured))
|
||||
deviceIndex := make(map[string]*DeviceInfo, len(scanned)+len(configured))
|
||||
|
||||
// Start with the newly scanned devices so we always surface fresh metadata,
|
||||
// but ensure we retain any previously verified parser assignment.
|
||||
for _, dev := range scanned {
|
||||
if dev == nil || dev.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Work on a copy so we can safely adjust metadata without mutating the
|
||||
// input slices that may be reused elsewhere.
|
||||
copyDev := *dev
|
||||
if prev := existingIndex[copyDev.Name]; prev != nil {
|
||||
preserveVerifiedType(©Dev, prev)
|
||||
}
|
||||
|
||||
finalDevices = append(finalDevices, ©Dev)
|
||||
deviceIndex[copyDev.Name] = finalDevices[len(finalDevices)-1]
|
||||
}
|
||||
|
||||
// Merge configured devices on top so users can override scan results (except
|
||||
// for verified type information).
|
||||
for _, dev := range configured {
|
||||
if dev == nil || dev.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if existingDev, ok := deviceIndex[dev.Name]; ok {
|
||||
// Only update the type if it has not been verified yet; otherwise we
|
||||
// keep the existing verified metadata intact.
|
||||
if dev.Type != "" && !existingDev.typeVerified {
|
||||
newType := strings.TrimSpace(dev.Type)
|
||||
existingDev.Type = newType
|
||||
existingDev.typeVerified = false
|
||||
existingDev.parserType = normalizeParserType(newType)
|
||||
}
|
||||
if dev.InfoName != "" {
|
||||
existingDev.InfoName = dev.InfoName
|
||||
}
|
||||
if dev.Protocol != "" {
|
||||
existingDev.Protocol = dev.Protocol
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
copyDev := *dev
|
||||
if prev := existingIndex[copyDev.Name]; prev != nil {
|
||||
preserveVerifiedType(©Dev, prev)
|
||||
} else if copyDev.Type != "" {
|
||||
copyDev.parserType = normalizeParserType(copyDev.Type)
|
||||
}
|
||||
|
||||
finalDevices = append(finalDevices, ©Dev)
|
||||
deviceIndex[copyDev.Name] = finalDevices[len(finalDevices)-1]
|
||||
}
|
||||
|
||||
return finalDevices
|
||||
}
|
||||
|
||||
// updateSmartDevices replaces the cached device list and prunes SMART data
|
||||
// entries whose backing device no longer exists.
|
||||
func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
sm.SmartDevices = devices
|
||||
|
||||
if len(sm.SmartDataMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
validNames := make(map[string]struct{}, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil || device.Name == "" {
|
||||
continue
|
||||
}
|
||||
validNames[device.Name] = struct{}{}
|
||||
}
|
||||
|
||||
for key, data := range sm.SmartDataMap {
|
||||
if data == nil {
|
||||
delete(sm.SmartDataMap, key)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validNames[data.DiskName]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(sm.SmartDataMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
// isVirtualDevice checks if a device is a virtual disk that should be filtered out
|
||||
func (sm *SmartManager) isVirtualDevice(data *smart.SmartInfoForSata) bool {
|
||||
vendorUpper := strings.ToUpper(data.ScsiVendor)
|
||||
productUpper := strings.ToUpper(data.ScsiProduct)
|
||||
modelUpper := strings.ToUpper(data.ModelName)
|
||||
|
||||
return sm.isVirtualDeviceFromStrings(vendorUpper, productUpper, modelUpper)
|
||||
}
|
||||
|
||||
// isVirtualDeviceNvme checks if an NVMe device is a virtual disk that should be filtered out
|
||||
func (sm *SmartManager) isVirtualDeviceNvme(data *smart.SmartInfoForNvme) bool {
|
||||
modelUpper := strings.ToUpper(data.ModelName)
|
||||
|
||||
return sm.isVirtualDeviceFromStrings(modelUpper)
|
||||
}
|
||||
|
||||
// isVirtualDeviceScsi checks if a SCSI device is a virtual disk that should be filtered out
|
||||
func (sm *SmartManager) isVirtualDeviceScsi(data *smart.SmartInfoForScsi) bool {
|
||||
vendorUpper := strings.ToUpper(data.ScsiVendor)
|
||||
productUpper := strings.ToUpper(data.ScsiProduct)
|
||||
modelUpper := strings.ToUpper(data.ScsiModelName)
|
||||
|
||||
return sm.isVirtualDeviceFromStrings(vendorUpper, productUpper, modelUpper)
|
||||
}
|
||||
|
||||
// isVirtualDeviceFromStrings checks if any of the provided strings indicate a virtual device
|
||||
func (sm *SmartManager) isVirtualDeviceFromStrings(fields ...string) bool {
|
||||
for _, field := range fields {
|
||||
fieldUpper := strings.ToUpper(field)
|
||||
switch {
|
||||
case strings.Contains(fieldUpper, "IET"), // iSCSI Enterprise Target
|
||||
strings.Contains(fieldUpper, "VIRTUAL"),
|
||||
strings.Contains(fieldUpper, "QEMU"),
|
||||
strings.Contains(fieldUpper, "VBOX"),
|
||||
strings.Contains(fieldUpper, "VMWARE"),
|
||||
strings.Contains(fieldUpper, "MSFT"): // Microsoft Hyper-V
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap
|
||||
// Returns hasValidData and exitStatus
|
||||
func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
||||
var data smart.SmartInfoForSata
|
||||
|
||||
if err := json.Unmarshal(output, &data); err != nil {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if data.SerialNumber == "" {
|
||||
slog.Debug("no serial number", "device", data.Device.Name)
|
||||
return false, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
// Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.)
|
||||
if sm.isVirtualDevice(&data) {
|
||||
slog.Debug("skipping smart", "device", data.Device.Name, "model", data.ModelName)
|
||||
return false, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
keyName := data.SerialNumber
|
||||
|
||||
// if device does not exist in SmartDataMap, initialize it
|
||||
if _, ok := sm.SmartDataMap[keyName]; !ok {
|
||||
sm.SmartDataMap[keyName] = &smart.SmartData{}
|
||||
}
|
||||
|
||||
// update SmartData
|
||||
smartData := sm.SmartDataMap[keyName]
|
||||
// smartData.ModelFamily = data.ModelFamily
|
||||
smartData.ModelName = data.ModelName
|
||||
smartData.SerialNumber = data.SerialNumber
|
||||
smartData.FirmwareVersion = data.FirmwareVersion
|
||||
smartData.Capacity = data.UserCapacity.Bytes
|
||||
smartData.Temperature = data.Temperature.Current
|
||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||
smartData.DiskName = data.Device.Name
|
||||
smartData.DiskType = data.Device.Type
|
||||
|
||||
// update SmartAttributes
|
||||
smartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table))
|
||||
for _, attr := range data.AtaSmartAttributes.Table {
|
||||
rawValue := uint64(attr.Raw.Value)
|
||||
if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok {
|
||||
rawValue = parsed
|
||||
}
|
||||
smartAttr := &smart.SmartAttribute{
|
||||
ID: attr.ID,
|
||||
Name: attr.Name,
|
||||
Value: attr.Value,
|
||||
Worst: attr.Worst,
|
||||
Threshold: attr.Thresh,
|
||||
RawValue: rawValue,
|
||||
RawString: attr.Raw.String,
|
||||
WhenFailed: attr.WhenFailed,
|
||||
}
|
||||
smartData.Attributes = append(smartData.Attributes, smartAttr)
|
||||
}
|
||||
sm.SmartDataMap[keyName] = smartData
|
||||
|
||||
return true, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
func getSmartStatus(temperature uint8, passed bool) string {
|
||||
if passed {
|
||||
return "PASSED"
|
||||
} else if temperature > 0 {
|
||||
return "FAILED"
|
||||
} else {
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
||||
var data smart.SmartInfoForScsi
|
||||
|
||||
if err := json.Unmarshal(output, &data); err != nil {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if data.SerialNumber == "" {
|
||||
slog.Debug("no serial number", "device", data.Device.Name)
|
||||
return false, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
// Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.)
|
||||
if sm.isVirtualDeviceScsi(&data) {
|
||||
slog.Debug("skipping smart", "device", data.Device.Name, "model", data.ScsiModelName)
|
||||
return false, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
keyName := data.SerialNumber
|
||||
if _, ok := sm.SmartDataMap[keyName]; !ok {
|
||||
sm.SmartDataMap[keyName] = &smart.SmartData{}
|
||||
}
|
||||
|
||||
smartData := sm.SmartDataMap[keyName]
|
||||
smartData.ModelName = data.ScsiModelName
|
||||
smartData.SerialNumber = data.SerialNumber
|
||||
smartData.FirmwareVersion = data.ScsiRevision
|
||||
smartData.Capacity = data.UserCapacity.Bytes
|
||||
smartData.Temperature = data.Temperature.Current
|
||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||
smartData.DiskName = data.Device.Name
|
||||
smartData.DiskType = data.Device.Type
|
||||
|
||||
attributes := make([]*smart.SmartAttribute, 0, 10)
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnMinutes", RawValue: data.PowerOnTime.Minutes})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "GrownDefectList", RawValue: data.ScsiGrownDefectList})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "StartStopCycles", RawValue: data.ScsiStartStopCycleCounter.AccumulatedStartStopCycles})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "LoadUnloadCycles", RawValue: data.ScsiStartStopCycleCounter.AccumulatedLoadUnloadCycles})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "StartStopSpecified", RawValue: data.ScsiStartStopCycleCounter.SpecifiedCycleCountOverDeviceLifetime})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "LoadUnloadSpecified", RawValue: data.ScsiStartStopCycleCounter.SpecifiedLoadUnloadCountOverDeviceLifetime})
|
||||
|
||||
readStats := data.ScsiErrorCounterLog.Read
|
||||
writeStats := data.ScsiErrorCounterLog.Write
|
||||
verifyStats := data.ScsiErrorCounterLog.Verify
|
||||
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "ReadTotalErrorsCorrected", RawValue: readStats.TotalErrorsCorrected})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "ReadTotalUncorrectedErrors", RawValue: readStats.TotalUncorrectedErrors})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "ReadCorrectionAlgorithmInvocations", RawValue: readStats.CorrectionAlgorithmInvocations})
|
||||
if val := parseScsiGigabytesProcessed(readStats.GigabytesProcessed); val >= 0 {
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "ReadGigabytesProcessed", RawValue: uint64(val)})
|
||||
}
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "WriteTotalErrorsCorrected", RawValue: writeStats.TotalErrorsCorrected})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "WriteTotalUncorrectedErrors", RawValue: writeStats.TotalUncorrectedErrors})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "WriteCorrectionAlgorithmInvocations", RawValue: writeStats.CorrectionAlgorithmInvocations})
|
||||
if val := parseScsiGigabytesProcessed(writeStats.GigabytesProcessed); val >= 0 {
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "WriteGigabytesProcessed", RawValue: uint64(val)})
|
||||
}
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyTotalErrorsCorrected", RawValue: verifyStats.TotalErrorsCorrected})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyTotalUncorrectedErrors", RawValue: verifyStats.TotalUncorrectedErrors})
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyCorrectionAlgorithmInvocations", RawValue: verifyStats.CorrectionAlgorithmInvocations})
|
||||
if val := parseScsiGigabytesProcessed(verifyStats.GigabytesProcessed); val >= 0 {
|
||||
attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyGigabytesProcessed", RawValue: uint64(val)})
|
||||
}
|
||||
|
||||
smartData.Attributes = attributes
|
||||
sm.SmartDataMap[keyName] = smartData
|
||||
|
||||
return true, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
func parseScsiGigabytesProcessed(value string) int64 {
|
||||
if value == "" {
|
||||
return -1
|
||||
}
|
||||
normalized := strings.ReplaceAll(value, ",", "")
|
||||
parsed, err := strconv.ParseInt(normalized, 10, 64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap
|
||||
// Returns hasValidData and exitStatus
|
||||
func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
|
||||
data := &smart.SmartInfoForNvme{}
|
||||
|
||||
if err := json.Unmarshal(output, &data); err != nil {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if data.SerialNumber == "" {
|
||||
slog.Debug("no serial number", "device", data.Device.Name)
|
||||
return false, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
// Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.)
|
||||
if sm.isVirtualDeviceNvme(data) {
|
||||
slog.Debug("skipping smart", "device", data.Device.Name, "model", data.ModelName)
|
||||
return false, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
keyName := data.SerialNumber
|
||||
|
||||
// if device does not exist in SmartDataMap, initialize it
|
||||
if _, ok := sm.SmartDataMap[keyName]; !ok {
|
||||
sm.SmartDataMap[keyName] = &smart.SmartData{}
|
||||
}
|
||||
|
||||
// update SmartData
|
||||
smartData := sm.SmartDataMap[keyName]
|
||||
smartData.ModelName = data.ModelName
|
||||
smartData.SerialNumber = data.SerialNumber
|
||||
smartData.FirmwareVersion = data.FirmwareVersion
|
||||
smartData.Capacity = data.UserCapacity.Bytes
|
||||
smartData.Temperature = data.NVMeSmartHealthInformationLog.Temperature
|
||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||
smartData.DiskName = data.Device.Name
|
||||
smartData.DiskType = data.Device.Type
|
||||
|
||||
// nvme attributes does not follow the same format as ata attributes,
|
||||
// so we manually map each field to SmartAttributes
|
||||
log := data.NVMeSmartHealthInformationLog
|
||||
smartData.Attributes = []*smart.SmartAttribute{
|
||||
{Name: "CriticalWarning", RawValue: uint64(log.CriticalWarning)},
|
||||
{Name: "Temperature", RawValue: uint64(log.Temperature)},
|
||||
{Name: "AvailableSpare", RawValue: uint64(log.AvailableSpare)},
|
||||
{Name: "AvailableSpareThreshold", RawValue: uint64(log.AvailableSpareThreshold)},
|
||||
{Name: "PercentageUsed", RawValue: uint64(log.PercentageUsed)},
|
||||
{Name: "DataUnitsRead", RawValue: log.DataUnitsRead},
|
||||
{Name: "DataUnitsWritten", RawValue: log.DataUnitsWritten},
|
||||
{Name: "HostReads", RawValue: uint64(log.HostReads)},
|
||||
{Name: "HostWrites", RawValue: uint64(log.HostWrites)},
|
||||
{Name: "ControllerBusyTime", RawValue: uint64(log.ControllerBusyTime)},
|
||||
{Name: "PowerCycles", RawValue: uint64(log.PowerCycles)},
|
||||
{Name: "PowerOnHours", RawValue: uint64(log.PowerOnHours)},
|
||||
{Name: "UnsafeShutdowns", RawValue: uint64(log.UnsafeShutdowns)},
|
||||
{Name: "MediaErrors", RawValue: uint64(log.MediaErrors)},
|
||||
{Name: "NumErrLogEntries", RawValue: uint64(log.NumErrLogEntries)},
|
||||
{Name: "WarningTempTime", RawValue: uint64(log.WarningTempTime)},
|
||||
{Name: "CriticalCompTime", RawValue: uint64(log.CriticalCompTime)},
|
||||
}
|
||||
|
||||
sm.SmartDataMap[keyName] = smartData
|
||||
|
||||
return true, data.Smartctl.ExitStatus
|
||||
}
|
||||
|
||||
// detectSmartctl checks if smartctl is installed, returns an error if not
|
||||
func (sm *SmartManager) detectSmartctl() error {
|
||||
if _, err := exec.LookPath("smartctl"); err == nil {
|
||||
slog.Debug("smartctl found")
|
||||
return nil
|
||||
}
|
||||
slog.Debug("smartctl not found")
|
||||
return errors.New("smartctl not found")
|
||||
}
|
||||
|
||||
// NewSmartManager creates and initializes a new SmartManager
|
||||
func NewSmartManager() (*SmartManager, error) {
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
if err := sm.detectSmartctl(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sm, nil
|
||||
}
|
||||
590
agent/smart_test.go
Normal file
590
agent/smart_test.go
Normal file
@@ -0,0 +1,590 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseSmartForScsi(t *testing.T) {
|
||||
fixturePath := filepath.Join("test-data", "smart", "scsi.json")
|
||||
data, err := os.ReadFile(fixturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed reading fixture: %v", err)
|
||||
}
|
||||
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
|
||||
hasData, exitStatus := sm.parseSmartForScsi(data)
|
||||
if !hasData {
|
||||
t.Fatalf("expected SCSI data to parse successfully")
|
||||
}
|
||||
if exitStatus != 0 {
|
||||
t.Fatalf("expected exit status 0, got %d", exitStatus)
|
||||
}
|
||||
|
||||
deviceData, ok := sm.SmartDataMap["9YHSDH9B"]
|
||||
if !ok {
|
||||
t.Fatalf("expected smart data entry for serial 9YHSDH9B")
|
||||
}
|
||||
|
||||
assert.Equal(t, deviceData.ModelName, "YADRO WUH721414AL4204")
|
||||
assert.Equal(t, deviceData.SerialNumber, "9YHSDH9B")
|
||||
assert.Equal(t, deviceData.FirmwareVersion, "C240")
|
||||
assert.Equal(t, deviceData.DiskName, "/dev/sde")
|
||||
assert.Equal(t, deviceData.DiskType, "scsi")
|
||||
assert.EqualValues(t, deviceData.Temperature, 34)
|
||||
assert.Equal(t, deviceData.SmartStatus, "PASSED")
|
||||
assert.EqualValues(t, deviceData.Capacity, 14000519643136)
|
||||
|
||||
if len(deviceData.Attributes) == 0 {
|
||||
t.Fatalf("expected attributes to be populated")
|
||||
}
|
||||
|
||||
assertAttrValue(t, deviceData.Attributes, "PowerOnHours", 458)
|
||||
assertAttrValue(t, deviceData.Attributes, "PowerOnMinutes", 25)
|
||||
assertAttrValue(t, deviceData.Attributes, "GrownDefectList", 0)
|
||||
assertAttrValue(t, deviceData.Attributes, "StartStopCycles", 2)
|
||||
assertAttrValue(t, deviceData.Attributes, "LoadUnloadCycles", 418)
|
||||
assertAttrValue(t, deviceData.Attributes, "ReadGigabytesProcessed", 3641)
|
||||
assertAttrValue(t, deviceData.Attributes, "WriteGigabytesProcessed", 2124590)
|
||||
assertAttrValue(t, deviceData.Attributes, "VerifyGigabytesProcessed", 0)
|
||||
}
|
||||
|
||||
func TestParseSmartForSata(t *testing.T) {
|
||||
fixturePath := filepath.Join("test-data", "smart", "sda.json")
|
||||
data, err := os.ReadFile(fixturePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
|
||||
hasData, exitStatus := sm.parseSmartForSata(data)
|
||||
require.True(t, hasData)
|
||||
assert.Equal(t, 64, exitStatus)
|
||||
|
||||
deviceData, ok := sm.SmartDataMap["9C40918040082"]
|
||||
require.True(t, ok, "expected smart data entry for serial 9C40918040082")
|
||||
|
||||
assert.Equal(t, "P3-2TB", deviceData.ModelName)
|
||||
assert.Equal(t, "X0104A0", deviceData.FirmwareVersion)
|
||||
assert.Equal(t, "/dev/sda", deviceData.DiskName)
|
||||
assert.Equal(t, "sat", deviceData.DiskType)
|
||||
assert.Equal(t, uint8(31), deviceData.Temperature)
|
||||
assert.Equal(t, "PASSED", deviceData.SmartStatus)
|
||||
assert.Equal(t, uint64(2048408248320), deviceData.Capacity)
|
||||
if assert.NotEmpty(t, deviceData.Attributes) {
|
||||
assertAttrValue(t, deviceData.Attributes, "Temperature_Celsius", 31)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
||||
jsonPayload := []byte(`{
|
||||
"smartctl": {"exit_status": 0},
|
||||
"device": {"name": "/dev/sdz", "type": "sat"},
|
||||
"model_name": "Example",
|
||||
"serial_number": "PARENTHESES123",
|
||||
"firmware_version": "1.0",
|
||||
"user_capacity": {"bytes": 1024},
|
||||
"smart_status": {"passed": true},
|
||||
"temperature": {"current": 25},
|
||||
"ata_smart_attributes": {
|
||||
"table": [
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Power_On_Hours",
|
||||
"value": 93,
|
||||
"worst": 55,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"raw": {
|
||||
"value": 57891864217128,
|
||||
"string": "39925 (212 206 0)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||
|
||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
||||
require.True(t, hasData)
|
||||
assert.Equal(t, 0, exitStatus)
|
||||
|
||||
data, ok := sm.SmartDataMap["PARENTHESES123"]
|
||||
require.True(t, ok)
|
||||
require.Len(t, data.Attributes, 1)
|
||||
|
||||
attr := data.Attributes[0]
|
||||
assert.Equal(t, uint64(39925), attr.RawValue)
|
||||
assert.Equal(t, "39925 (212 206 0)", attr.RawString)
|
||||
}
|
||||
|
||||
func TestParseSmartForNvme(t *testing.T) {
|
||||
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
||||
data, err := os.ReadFile(fixturePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
|
||||
hasData, exitStatus := sm.parseSmartForNvme(data)
|
||||
require.True(t, hasData)
|
||||
assert.Equal(t, 0, exitStatus)
|
||||
|
||||
deviceData, ok := sm.SmartDataMap["2024031600129"]
|
||||
require.True(t, ok, "expected smart data entry for serial 2024031600129")
|
||||
|
||||
assert.Equal(t, "PELADN 512GB", deviceData.ModelName)
|
||||
assert.Equal(t, "VC2S038E", deviceData.FirmwareVersion)
|
||||
assert.Equal(t, "/dev/nvme0", deviceData.DiskName)
|
||||
assert.Equal(t, "nvme", deviceData.DiskType)
|
||||
assert.Equal(t, uint8(61), deviceData.Temperature)
|
||||
assert.Equal(t, "PASSED", deviceData.SmartStatus)
|
||||
assert.Equal(t, uint64(512110190592), deviceData.Capacity)
|
||||
if assert.NotEmpty(t, deviceData.Attributes) {
|
||||
assertAttrValue(t, deviceData.Attributes, "PercentageUsed", 0)
|
||||
assertAttrValue(t, deviceData.Attributes, "DataUnitsWritten", 16040567)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDataForDevice(t *testing.T) {
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: map[string]*smart.SmartData{
|
||||
"serial-1": {DiskName: "/dev/sda"},
|
||||
"serial-2": nil,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, sm.hasDataForDevice("/dev/sda"))
|
||||
assert.False(t, sm.hasDataForDevice("/dev/sdb"))
|
||||
}
|
||||
|
||||
func TestDevicesSnapshotReturnsCopy(t *testing.T) {
|
||||
originalDevice := &DeviceInfo{Name: "/dev/sda"}
|
||||
sm := &SmartManager{
|
||||
SmartDevices: []*DeviceInfo{
|
||||
originalDevice,
|
||||
{Name: "/dev/sdb"},
|
||||
},
|
||||
}
|
||||
|
||||
snapshot := sm.devicesSnapshot()
|
||||
require.Len(t, snapshot, 2)
|
||||
|
||||
sm.SmartDevices[0] = &DeviceInfo{Name: "/dev/sdz"}
|
||||
assert.Equal(t, "/dev/sda", snapshot[0].Name)
|
||||
|
||||
snapshot[1] = &DeviceInfo{Name: "/dev/nvme0"}
|
||||
assert.Equal(t, "/dev/sdb", sm.SmartDevices[1].Name)
|
||||
|
||||
sm.SmartDevices = append(sm.SmartDevices, &DeviceInfo{Name: "/dev/nvme1"})
|
||||
assert.Len(t, snapshot, 2)
|
||||
}
|
||||
|
||||
func TestScanDevicesWithEnvOverride(t *testing.T) {
|
||||
t.Setenv("SMART_DEVICES", "/dev/sda:sat, /dev/nvme0:nvme")
|
||||
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
|
||||
err := sm.ScanDevices(true)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, sm.SmartDevices, 2)
|
||||
assert.Equal(t, "/dev/sda", sm.SmartDevices[0].Name)
|
||||
assert.Equal(t, "sat", sm.SmartDevices[0].Type)
|
||||
assert.Equal(t, "/dev/nvme0", sm.SmartDevices[1].Name)
|
||||
assert.Equal(t, "nvme", sm.SmartDevices[1].Type)
|
||||
}
|
||||
|
||||
func TestScanDevicesWithEnvOverrideInvalid(t *testing.T) {
|
||||
t.Setenv("SMART_DEVICES", ":sat")
|
||||
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
|
||||
err := sm.ScanDevices(true)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestScanDevicesWithEnvOverrideEmpty(t *testing.T) {
|
||||
t.Setenv("SMART_DEVICES", " ")
|
||||
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: make(map[string]*smart.SmartData),
|
||||
}
|
||||
|
||||
err := sm.ScanDevices(true)
|
||||
assert.ErrorIs(t, err, errNoValidSmartData)
|
||||
assert.Empty(t, sm.SmartDevices)
|
||||
}
|
||||
|
||||
func TestSmartctlArgsWithoutType(t *testing.T) {
|
||||
device := &DeviceInfo{Name: "/dev/sda"}
|
||||
|
||||
sm := &SmartManager{}
|
||||
|
||||
args := sm.smartctlArgs(device, true)
|
||||
assert.Equal(t, []string{"-a", "--json=c", "-n", "standby", "/dev/sda"}, args)
|
||||
}
|
||||
|
||||
func TestSmartctlArgs(t *testing.T) {
|
||||
sm := &SmartManager{}
|
||||
|
||||
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
|
||||
assert.Equal(t,
|
||||
[]string{"-d", "sat", "-a", "--json=c", "-n", "standby", "/dev/sda"},
|
||||
sm.smartctlArgs(sataDevice, true),
|
||||
)
|
||||
|
||||
assert.Equal(t,
|
||||
[]string{"-d", "sat", "-a", "--json=c", "/dev/sda"},
|
||||
sm.smartctlArgs(sataDevice, false),
|
||||
)
|
||||
|
||||
assert.Equal(t,
|
||||
[]string{"-a", "--json=c", "-n", "standby"},
|
||||
sm.smartctlArgs(nil, true),
|
||||
)
|
||||
}
|
||||
|
||||
func TestResolveRefreshError(t *testing.T) {
|
||||
scanErr := errors.New("scan failed")
|
||||
collectErr := errors.New("collect failed")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
devices []*DeviceInfo
|
||||
data map[string]*smart.SmartData
|
||||
scanErr error
|
||||
collectErr error
|
||||
expectedErr error
|
||||
expectNoErr bool
|
||||
}{
|
||||
{
|
||||
name: "no devices returns scan error",
|
||||
devices: nil,
|
||||
data: make(map[string]*smart.SmartData),
|
||||
scanErr: scanErr,
|
||||
expectedErr: scanErr,
|
||||
},
|
||||
{
|
||||
name: "has data ignores errors",
|
||||
devices: []*DeviceInfo{{Name: "/dev/sda"}},
|
||||
data: map[string]*smart.SmartData{"serial": {}},
|
||||
scanErr: scanErr,
|
||||
collectErr: collectErr,
|
||||
expectNoErr: true,
|
||||
},
|
||||
{
|
||||
name: "collect error preferred",
|
||||
devices: []*DeviceInfo{{Name: "/dev/sda"}},
|
||||
data: make(map[string]*smart.SmartData),
|
||||
collectErr: collectErr,
|
||||
expectedErr: collectErr,
|
||||
},
|
||||
{
|
||||
name: "scan error returned when no data",
|
||||
devices: []*DeviceInfo{{Name: "/dev/sda"}},
|
||||
data: make(map[string]*smart.SmartData),
|
||||
scanErr: scanErr,
|
||||
expectedErr: scanErr,
|
||||
},
|
||||
{
|
||||
name: "no errors returns sentinel",
|
||||
devices: []*DeviceInfo{{Name: "/dev/sda"}},
|
||||
data: make(map[string]*smart.SmartData),
|
||||
expectedErr: errNoValidSmartData,
|
||||
},
|
||||
{
|
||||
name: "no devices collect error",
|
||||
devices: nil,
|
||||
data: make(map[string]*smart.SmartData),
|
||||
collectErr: collectErr,
|
||||
expectedErr: collectErr,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sm := &SmartManager{
|
||||
SmartDevices: tt.devices,
|
||||
SmartDataMap: tt.data,
|
||||
}
|
||||
|
||||
err := sm.resolveRefreshError(tt.scanErr, tt.collectErr)
|
||||
if tt.expectNoErr {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
if tt.expectedErr == nil {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseScan(t *testing.T) {
|
||||
sm := &SmartManager{
|
||||
SmartDataMap: map[string]*smart.SmartData{
|
||||
"serial-active": {DiskName: "/dev/sda"},
|
||||
"serial-stale": {DiskName: "/dev/sdb"},
|
||||
},
|
||||
}
|
||||
|
||||
scanJSON := []byte(`{
|
||||
"devices": [
|
||||
{"name": "/dev/sda", "type": "sat", "info_name": "/dev/sda [SAT]", "protocol": "ATA"},
|
||||
{"name": "/dev/nvme0", "type": "nvme", "info_name": "/dev/nvme0", "protocol": "NVMe"}
|
||||
]
|
||||
}`)
|
||||
|
||||
devices, hasData := sm.parseScan(scanJSON)
|
||||
assert.True(t, hasData)
|
||||
|
||||
sm.updateSmartDevices(devices)
|
||||
|
||||
require.Len(t, sm.SmartDevices, 2)
|
||||
assert.Equal(t, "/dev/sda", sm.SmartDevices[0].Name)
|
||||
assert.Equal(t, "sat", sm.SmartDevices[0].Type)
|
||||
assert.Equal(t, "/dev/nvme0", sm.SmartDevices[1].Name)
|
||||
assert.Equal(t, "nvme", sm.SmartDevices[1].Type)
|
||||
|
||||
_, activeExists := sm.SmartDataMap["serial-active"]
|
||||
assert.True(t, activeExists, "active smart data should be preserved when device path remains")
|
||||
|
||||
_, staleExists := sm.SmartDataMap["serial-stale"]
|
||||
assert.False(t, staleExists, "stale smart data entry should be removed when device path disappears")
|
||||
}
|
||||
|
||||
func TestMergeDeviceListsPrefersConfigured(t *testing.T) {
|
||||
scanned := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "sat", InfoName: "scan-info", Protocol: "ATA"},
|
||||
{Name: "/dev/nvme0", Type: "nvme"},
|
||||
}
|
||||
|
||||
configured := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "sat-override"},
|
||||
{Name: "/dev/sdb", Type: "sat"},
|
||||
}
|
||||
|
||||
merged := mergeDeviceLists(nil, scanned, configured)
|
||||
require.Len(t, merged, 3)
|
||||
|
||||
byName := make(map[string]*DeviceInfo, len(merged))
|
||||
for _, dev := range merged {
|
||||
byName[dev.Name] = dev
|
||||
}
|
||||
|
||||
require.Contains(t, byName, "/dev/sda")
|
||||
assert.Equal(t, "sat-override", byName["/dev/sda"].Type, "configured type should override scanned type")
|
||||
assert.Equal(t, "scan-info", byName["/dev/sda"].InfoName, "scan metadata should be preserved when config does not provide it")
|
||||
|
||||
require.Contains(t, byName, "/dev/nvme0")
|
||||
assert.Equal(t, "nvme", byName["/dev/nvme0"].Type)
|
||||
|
||||
require.Contains(t, byName, "/dev/sdb")
|
||||
assert.Equal(t, "sat", byName["/dev/sdb"].Type)
|
||||
}
|
||||
|
||||
func TestMergeDeviceListsPreservesVerification(t *testing.T) {
|
||||
existing := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "sat+megaraid", parserType: "sat", typeVerified: true},
|
||||
}
|
||||
|
||||
scanned := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "nvme"},
|
||||
}
|
||||
|
||||
merged := mergeDeviceLists(existing, scanned, nil)
|
||||
require.Len(t, merged, 1)
|
||||
|
||||
device := merged[0]
|
||||
assert.True(t, device.typeVerified)
|
||||
assert.Equal(t, "sat", device.parserType)
|
||||
assert.Equal(t, "sat+megaraid", device.Type)
|
||||
}
|
||||
|
||||
func TestMergeDeviceListsUpdatesTypeWhenUnverified(t *testing.T) {
|
||||
existing := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "sat", parserType: "sat", typeVerified: false},
|
||||
}
|
||||
|
||||
scanned := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "nvme"},
|
||||
}
|
||||
|
||||
merged := mergeDeviceLists(existing, scanned, nil)
|
||||
require.Len(t, merged, 1)
|
||||
|
||||
device := merged[0]
|
||||
assert.False(t, device.typeVerified)
|
||||
assert.Equal(t, "nvme", device.Type)
|
||||
assert.Equal(t, "", device.parserType)
|
||||
}
|
||||
|
||||
func TestParseSmartOutputMarksVerified(t *testing.T) {
|
||||
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
||||
data, err := os.ReadFile(fixturePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||
device := &DeviceInfo{Name: "/dev/nvme0"}
|
||||
|
||||
require.True(t, sm.parseSmartOutput(device, data))
|
||||
assert.Equal(t, "nvme", device.Type)
|
||||
assert.Equal(t, "nvme", device.parserType)
|
||||
assert.True(t, device.typeVerified)
|
||||
}
|
||||
|
||||
func TestParseSmartOutputKeepsCustomType(t *testing.T) {
|
||||
fixturePath := filepath.Join("test-data", "smart", "sda.json")
|
||||
data, err := os.ReadFile(fixturePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||
device := &DeviceInfo{Name: "/dev/sda", Type: "sat+megaraid"}
|
||||
|
||||
require.True(t, sm.parseSmartOutput(device, data))
|
||||
assert.Equal(t, "sat+megaraid", device.Type)
|
||||
assert.Equal(t, "sat", device.parserType)
|
||||
assert.True(t, device.typeVerified)
|
||||
}
|
||||
|
||||
func TestParseSmartOutputResetsVerificationOnFailure(t *testing.T) {
|
||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||
device := &DeviceInfo{Name: "/dev/sda", Type: "sat", parserType: "sat", typeVerified: true}
|
||||
|
||||
assert.False(t, sm.parseSmartOutput(device, []byte("not json")))
|
||||
assert.False(t, device.typeVerified)
|
||||
assert.Equal(t, "sat", device.parserType)
|
||||
}
|
||||
|
||||
func assertAttrValue(t *testing.T, attributes []*smart.SmartAttribute, name string, expected uint64) {
|
||||
t.Helper()
|
||||
attr := findAttr(attributes, name)
|
||||
if attr == nil {
|
||||
t.Fatalf("expected attribute %s to be present", name)
|
||||
}
|
||||
if attr.RawValue != expected {
|
||||
t.Fatalf("unexpected attribute %s value: got %d, want %d", name, attr.RawValue, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func findAttr(attributes []*smart.SmartAttribute, name string) *smart.SmartAttribute {
|
||||
for _, attr := range attributes {
|
||||
if attr != nil && attr.Name == name {
|
||||
return attr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestIsVirtualDevice(t *testing.T) {
|
||||
sm := &SmartManager{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vendor string
|
||||
product string
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"regular drive", "SEAGATE", "ST1000DM003", "ST1000DM003-1CH162", false},
|
||||
{"qemu virtual", "QEMU", "QEMU HARDDISK", "QEMU HARDDISK", true},
|
||||
{"virtualbox virtual", "VBOX", "HARDDISK", "VBOX HARDDISK", true},
|
||||
{"vmware virtual", "VMWARE", "Virtual disk", "VMWARE Virtual disk", true},
|
||||
{"virtual in model", "ATA", "VIRTUAL", "VIRTUAL DISK", true},
|
||||
{"iet virtual", "IET", "VIRTUAL-DISK", "VIRTUAL-DISK", true},
|
||||
{"hyper-v virtual", "MSFT", "VIRTUAL HD", "VIRTUAL HD", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := &smart.SmartInfoForSata{
|
||||
ScsiVendor: tt.vendor,
|
||||
ScsiProduct: tt.product,
|
||||
ModelName: tt.model,
|
||||
}
|
||||
result := sm.isVirtualDevice(data)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVirtualDeviceNvme(t *testing.T) {
|
||||
sm := &SmartManager{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"regular nvme", "Samsung SSD 970 EVO Plus 1TB", false},
|
||||
{"qemu virtual", "QEMU NVMe Ctrl", true},
|
||||
{"virtualbox virtual", "VBOX NVMe", true},
|
||||
{"vmware virtual", "VMWARE NVMe", true},
|
||||
{"virtual in model", "Virtual NVMe Device", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := &smart.SmartInfoForNvme{
|
||||
ModelName: tt.model,
|
||||
}
|
||||
result := sm.isVirtualDeviceNvme(data)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVirtualDeviceScsi(t *testing.T) {
|
||||
sm := &SmartManager{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vendor string
|
||||
product string
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"regular scsi", "SEAGATE", "ST1000DM003", "ST1000DM003-1CH162", false},
|
||||
{"qemu virtual", "QEMU", "QEMU HARDDISK", "QEMU HARDDISK", true},
|
||||
{"virtualbox virtual", "VBOX", "HARDDISK", "VBOX HARDDISK", true},
|
||||
{"vmware virtual", "VMWARE", "Virtual disk", "VMWARE Virtual disk", true},
|
||||
{"virtual in model", "ATA", "VIRTUAL", "VIRTUAL DISK", true},
|
||||
{"iet virtual", "IET", "VIRTUAL-DISK", "VIRTUAL-DISK", true},
|
||||
{"hyper-v virtual", "MSFT", "VIRTUAL HD", "VIRTUAL HD", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := &smart.SmartInfoForScsi{
|
||||
ScsiVendor: tt.vendor,
|
||||
ScsiProduct: tt.product,
|
||||
ScsiModelName: tt.model,
|
||||
}
|
||||
result := sm.isVirtualDeviceScsi(data)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,16 +78,38 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
var systemStats system.Stats
|
||||
|
||||
// battery
|
||||
if battery.HasReadableBattery() {
|
||||
systemStats.Battery[0], systemStats.Battery[1], _ = battery.GetBatteryStats()
|
||||
if batteryPercent, batteryState, err := battery.GetBatteryStats(); err == nil {
|
||||
systemStats.Battery[0] = batteryPercent
|
||||
systemStats.Battery[1] = batteryState
|
||||
}
|
||||
|
||||
// cpu percent
|
||||
cpuPercent, err := getCpuPercent(cacheTimeMs)
|
||||
// cpu metrics
|
||||
cpuMetrics, err := getCpuMetrics(cacheTimeMs)
|
||||
if err == nil {
|
||||
systemStats.Cpu = twoDecimals(cpuPercent)
|
||||
systemStats.Cpu = twoDecimals(cpuMetrics.Total)
|
||||
systemStats.CpuBreakdown = []float64{
|
||||
twoDecimals(cpuMetrics.User),
|
||||
twoDecimals(cpuMetrics.System),
|
||||
twoDecimals(cpuMetrics.Iowait),
|
||||
twoDecimals(cpuMetrics.Steal),
|
||||
twoDecimals(cpuMetrics.Idle),
|
||||
}
|
||||
} else {
|
||||
slog.Error("Error getting cpu percent", "err", err)
|
||||
slog.Error("Error getting cpu metrics", "err", err)
|
||||
}
|
||||
|
||||
if topProcess, err := getTopCpuProcess(cacheTimeMs); err == nil {
|
||||
if topProcess != nil {
|
||||
topProcess.Percent = twoDecimals(topProcess.Percent)
|
||||
systemStats.TopCpuProcess = topProcess
|
||||
}
|
||||
} else {
|
||||
slog.Error("Error getting top cpu process", "err", err)
|
||||
}
|
||||
|
||||
// per-core cpu usage
|
||||
if perCoreUsage, err := getPerCoreCpuUsage(cacheTimeMs); err == nil {
|
||||
systemStats.CpuCoresUsage = perCoreUsage
|
||||
}
|
||||
|
||||
// load average
|
||||
|
||||
272
agent/test-data/smart/nvme0.json
Normal file
272
agent/test-data/smart/nvme0.json
Normal file
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"json_format_version": [
|
||||
1,
|
||||
0
|
||||
],
|
||||
"smartctl": {
|
||||
"version": [
|
||||
7,
|
||||
5
|
||||
],
|
||||
"pre_release": false,
|
||||
"svn_revision": "5714",
|
||||
"platform_info": "x86_64-linux-6.17.1-2-cachyos",
|
||||
"build_info": "(local build)",
|
||||
"argv": [
|
||||
"smartctl",
|
||||
"-aj",
|
||||
"/dev/nvme0"
|
||||
],
|
||||
"exit_status": 0
|
||||
},
|
||||
"local_time": {
|
||||
"time_t": 1761507494,
|
||||
"asctime": "Sun Oct 26 15:38:14 2025 EDT"
|
||||
},
|
||||
"device": {
|
||||
"name": "/dev/nvme0",
|
||||
"info_name": "/dev/nvme0",
|
||||
"type": "nvme",
|
||||
"protocol": "NVMe"
|
||||
},
|
||||
"model_name": "PELADN 512GB",
|
||||
"serial_number": "2024031600129",
|
||||
"firmware_version": "VC2S038E",
|
||||
"nvme_pci_vendor": {
|
||||
"id": 4332,
|
||||
"subsystem_id": 4332
|
||||
},
|
||||
"nvme_ieee_oui_identifier": 57420,
|
||||
"nvme_controller_id": 1,
|
||||
"nvme_version": {
|
||||
"string": "1.4",
|
||||
"value": 66560
|
||||
},
|
||||
"nvme_number_of_namespaces": 1,
|
||||
"nvme_namespaces": [
|
||||
{
|
||||
"id": 1,
|
||||
"size": {
|
||||
"blocks": 1000215216,
|
||||
"bytes": 512110190592
|
||||
},
|
||||
"capacity": {
|
||||
"blocks": 1000215216,
|
||||
"bytes": 512110190592
|
||||
},
|
||||
"utilization": {
|
||||
"blocks": 1000215216,
|
||||
"bytes": 512110190592
|
||||
},
|
||||
"formatted_lba_size": 512,
|
||||
"eui64": {
|
||||
"oui": 57420,
|
||||
"ext_id": 112094110470
|
||||
},
|
||||
"features": {
|
||||
"value": 0,
|
||||
"thin_provisioning": false,
|
||||
"na_fields": false,
|
||||
"dealloc_or_unwritten_block_error": false,
|
||||
"uid_reuse": false,
|
||||
"np_fields": false,
|
||||
"other": 0
|
||||
},
|
||||
"lba_formats": [
|
||||
{
|
||||
"formatted": true,
|
||||
"data_bytes": 512,
|
||||
"metadata_bytes": 0,
|
||||
"relative_performance": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"user_capacity": {
|
||||
"blocks": 1000215216,
|
||||
"bytes": 512110190592
|
||||
},
|
||||
"logical_block_size": 512,
|
||||
"smart_support": {
|
||||
"available": true,
|
||||
"enabled": true
|
||||
},
|
||||
"nvme_firmware_update_capabilities": {
|
||||
"value": 2,
|
||||
"slots": 1,
|
||||
"first_slot_is_read_only": false,
|
||||
"activiation_without_reset": false,
|
||||
"multiple_update_detection": false,
|
||||
"other": 0
|
||||
},
|
||||
"nvme_optional_admin_commands": {
|
||||
"value": 23,
|
||||
"security_send_receive": true,
|
||||
"format_nvm": true,
|
||||
"firmware_download": true,
|
||||
"namespace_management": false,
|
||||
"self_test": true,
|
||||
"directives": false,
|
||||
"mi_send_receive": false,
|
||||
"virtualization_management": false,
|
||||
"doorbell_buffer_config": false,
|
||||
"get_lba_status": false,
|
||||
"command_and_feature_lockdown": false,
|
||||
"other": 0
|
||||
},
|
||||
"nvme_optional_nvm_commands": {
|
||||
"value": 94,
|
||||
"compare": false,
|
||||
"write_uncorrectable": true,
|
||||
"dataset_management": true,
|
||||
"write_zeroes": true,
|
||||
"save_select_feature_nonzero": true,
|
||||
"reservations": false,
|
||||
"timestamp": true,
|
||||
"verify": false,
|
||||
"copy": false,
|
||||
"other": 0
|
||||
},
|
||||
"nvme_log_page_attributes": {
|
||||
"value": 2,
|
||||
"smart_health_per_namespace": false,
|
||||
"commands_effects_log": true,
|
||||
"extended_get_log_page_cmd": false,
|
||||
"telemetry_log": false,
|
||||
"persistent_event_log": false,
|
||||
"supported_log_pages_log": false,
|
||||
"telemetry_data_area_4": false,
|
||||
"other": 0
|
||||
},
|
||||
"nvme_maximum_data_transfer_pages": 32,
|
||||
"nvme_composite_temperature_threshold": {
|
||||
"warning": 100,
|
||||
"critical": 110
|
||||
},
|
||||
"temperature": {
|
||||
"op_limit_max": 100,
|
||||
"critical_limit_max": 110,
|
||||
"current": 61
|
||||
},
|
||||
"nvme_power_states": [
|
||||
{
|
||||
"non_operational_state": false,
|
||||
"relative_read_latency": 0,
|
||||
"relative_read_throughput": 0,
|
||||
"relative_write_latency": 0,
|
||||
"relative_write_throughput": 0,
|
||||
"entry_latency_us": 230000,
|
||||
"exit_latency_us": 50000,
|
||||
"max_power": {
|
||||
"value": 800,
|
||||
"scale": 2,
|
||||
"units_per_watt": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"non_operational_state": false,
|
||||
"relative_read_latency": 1,
|
||||
"relative_read_throughput": 1,
|
||||
"relative_write_latency": 1,
|
||||
"relative_write_throughput": 1,
|
||||
"entry_latency_us": 4000,
|
||||
"exit_latency_us": 50000,
|
||||
"max_power": {
|
||||
"value": 400,
|
||||
"scale": 2,
|
||||
"units_per_watt": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"non_operational_state": false,
|
||||
"relative_read_latency": 2,
|
||||
"relative_read_throughput": 2,
|
||||
"relative_write_latency": 2,
|
||||
"relative_write_throughput": 2,
|
||||
"entry_latency_us": 4000,
|
||||
"exit_latency_us": 250000,
|
||||
"max_power": {
|
||||
"value": 300,
|
||||
"scale": 2,
|
||||
"units_per_watt": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"non_operational_state": true,
|
||||
"relative_read_latency": 3,
|
||||
"relative_read_throughput": 3,
|
||||
"relative_write_latency": 3,
|
||||
"relative_write_throughput": 3,
|
||||
"entry_latency_us": 5000,
|
||||
"exit_latency_us": 10000,
|
||||
"max_power": {
|
||||
"value": 300,
|
||||
"scale": 1,
|
||||
"units_per_watt": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"non_operational_state": true,
|
||||
"relative_read_latency": 4,
|
||||
"relative_read_throughput": 4,
|
||||
"relative_write_latency": 4,
|
||||
"relative_write_throughput": 4,
|
||||
"entry_latency_us": 54000,
|
||||
"exit_latency_us": 45000,
|
||||
"max_power": {
|
||||
"value": 50,
|
||||
"scale": 1,
|
||||
"units_per_watt": 10000
|
||||
}
|
||||
}
|
||||
],
|
||||
"smart_status": {
|
||||
"passed": true,
|
||||
"nvme": {
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"nvme_smart_health_information_log": {
|
||||
"nsid": -1,
|
||||
"critical_warning": 0,
|
||||
"temperature": 61,
|
||||
"available_spare": 100,
|
||||
"available_spare_threshold": 32,
|
||||
"percentage_used": 0,
|
||||
"data_units_read": 6573104,
|
||||
"data_units_written": 16040567,
|
||||
"host_reads": 63241130,
|
||||
"host_writes": 253050006,
|
||||
"controller_busy_time": 0,
|
||||
"power_cycles": 430,
|
||||
"power_on_hours": 4399,
|
||||
"unsafe_shutdowns": 44,
|
||||
"media_errors": 0,
|
||||
"num_err_log_entries": 0,
|
||||
"warning_temp_time": 0,
|
||||
"critical_comp_time": 0
|
||||
},
|
||||
"spare_available": {
|
||||
"current_percent": 100,
|
||||
"threshold_percent": 32
|
||||
},
|
||||
"endurance_used": {
|
||||
"current_percent": 0
|
||||
},
|
||||
"power_cycle_count": 430,
|
||||
"power_on_time": {
|
||||
"hours": 4399
|
||||
},
|
||||
"nvme_error_information_log": {
|
||||
"size": 8,
|
||||
"read": 8,
|
||||
"unread": 0
|
||||
},
|
||||
"nvme_self_test_log": {
|
||||
"nsid": -1,
|
||||
"current_self_test_operation": {
|
||||
"value": 0,
|
||||
"string": "No self-test in progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
36
agent/test-data/smart/scan.json
Normal file
36
agent/test-data/smart/scan.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"json_format_version": [
|
||||
1,
|
||||
0
|
||||
],
|
||||
"smartctl": {
|
||||
"version": [
|
||||
7,
|
||||
5
|
||||
],
|
||||
"pre_release": false,
|
||||
"svn_revision": "5714",
|
||||
"platform_info": "x86_64-linux-6.17.1-2-cachyos",
|
||||
"build_info": "(local build)",
|
||||
"argv": [
|
||||
"smartctl",
|
||||
"--scan",
|
||||
"-j"
|
||||
],
|
||||
"exit_status": 0
|
||||
},
|
||||
"devices": [
|
||||
{
|
||||
"name": "/dev/sda",
|
||||
"info_name": "/dev/sda [SAT]",
|
||||
"type": "sat",
|
||||
"protocol": "ATA"
|
||||
},
|
||||
{
|
||||
"name": "/dev/nvme0",
|
||||
"info_name": "/dev/nvme0",
|
||||
"type": "nvme",
|
||||
"protocol": "NVMe"
|
||||
}
|
||||
]
|
||||
}
|
||||
125
agent/test-data/smart/scsi.json
Normal file
125
agent/test-data/smart/scsi.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"json_format_version": [
|
||||
1,
|
||||
0
|
||||
],
|
||||
"smartctl": {
|
||||
"version": [
|
||||
7,
|
||||
3
|
||||
],
|
||||
"svn_revision": "5338",
|
||||
"platform_info": "x86_64-linux-6.12.43+deb12-amd64",
|
||||
"build_info": "(local build)",
|
||||
"argv": [
|
||||
"smartctl",
|
||||
"-aj",
|
||||
"/dev/sde"
|
||||
],
|
||||
"exit_status": 0
|
||||
},
|
||||
"local_time": {
|
||||
"time_t": 1761502142,
|
||||
"asctime": "Sun Oct 21 21:09:02 2025 MSK"
|
||||
},
|
||||
"device": {
|
||||
"name": "/dev/sde",
|
||||
"info_name": "/dev/sde",
|
||||
"type": "scsi",
|
||||
"protocol": "SCSI"
|
||||
},
|
||||
"scsi_vendor": "YADRO",
|
||||
"scsi_product": "WUH721414AL4204",
|
||||
"scsi_model_name": "YADRO WUH721414AL4204",
|
||||
"scsi_revision": "C240",
|
||||
"scsi_version": "SPC-4",
|
||||
"user_capacity": {
|
||||
"blocks": 3418095616,
|
||||
"bytes": 14000519643136
|
||||
},
|
||||
"logical_block_size": 4096,
|
||||
"scsi_lb_provisioning": {
|
||||
"name": "fully provisioned",
|
||||
"value": 0,
|
||||
"management_enabled": {
|
||||
"name": "LBPME",
|
||||
"value": 0
|
||||
},
|
||||
"read_zeros": {
|
||||
"name": "LBPRZ",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"rotation_rate": 7200,
|
||||
"form_factor": {
|
||||
"scsi_value": 2,
|
||||
"name": "3.5 inches"
|
||||
},
|
||||
"logical_unit_id": "0x5000cca29063dc00",
|
||||
"serial_number": "9YHSDH9B",
|
||||
"device_type": {
|
||||
"scsi_terminology": "Peripheral Device Type [PDT]",
|
||||
"scsi_value": 0,
|
||||
"name": "disk"
|
||||
},
|
||||
"scsi_transport_protocol": {
|
||||
"name": "SAS (SPL-4)",
|
||||
"value": 6
|
||||
},
|
||||
"smart_support": {
|
||||
"available": true,
|
||||
"enabled": true
|
||||
},
|
||||
"temperature_warning": {
|
||||
"enabled": true
|
||||
},
|
||||
"smart_status": {
|
||||
"passed": true
|
||||
},
|
||||
"temperature": {
|
||||
"current": 34,
|
||||
"drive_trip": 85
|
||||
},
|
||||
"power_on_time": {
|
||||
"hours": 458,
|
||||
"minutes": 25
|
||||
},
|
||||
"scsi_start_stop_cycle_counter": {
|
||||
"year_of_manufacture": "2022",
|
||||
"week_of_manufacture": "41",
|
||||
"specified_cycle_count_over_device_lifetime": 50000,
|
||||
"accumulated_start_stop_cycles": 2,
|
||||
"specified_load_unload_count_over_device_lifetime": 600000,
|
||||
"accumulated_load_unload_cycles": 418
|
||||
},
|
||||
"scsi_grown_defect_list": 0,
|
||||
"scsi_error_counter_log": {
|
||||
"read": {
|
||||
"errors_corrected_by_eccfast": 0,
|
||||
"errors_corrected_by_eccdelayed": 0,
|
||||
"errors_corrected_by_rereads_rewrites": 0,
|
||||
"total_errors_corrected": 0,
|
||||
"correction_algorithm_invocations": 346,
|
||||
"gigabytes_processed": "3,641",
|
||||
"total_uncorrected_errors": 0
|
||||
},
|
||||
"write": {
|
||||
"errors_corrected_by_eccfast": 0,
|
||||
"errors_corrected_by_eccdelayed": 0,
|
||||
"errors_corrected_by_rereads_rewrites": 0,
|
||||
"total_errors_corrected": 0,
|
||||
"correction_algorithm_invocations": 4052,
|
||||
"gigabytes_processed": "2124,590",
|
||||
"total_uncorrected_errors": 0
|
||||
},
|
||||
"verify": {
|
||||
"errors_corrected_by_eccfast": 0,
|
||||
"errors_corrected_by_eccdelayed": 0,
|
||||
"errors_corrected_by_rereads_rewrites": 0,
|
||||
"total_errors_corrected": 0,
|
||||
"correction_algorithm_invocations": 223,
|
||||
"gigabytes_processed": "0,000",
|
||||
"total_uncorrected_errors": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
1013
agent/test-data/smart/sda.json
Normal file
1013
agent/test-data/smart/sda.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
// Version is the current version of the application.
|
||||
Version = "0.13.1"
|
||||
Version = "0.15.3"
|
||||
// AppName is the name of the application.
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
39
go.mod
39
go.mod
@@ -1,9 +1,6 @@
|
||||
module github.com/henrygd/beszel
|
||||
|
||||
go 1.25.1
|
||||
|
||||
// lock shoutrrr to specific version to allow review before updating
|
||||
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.9.1
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
@@ -12,16 +9,16 @@ require (
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lxzan/gws v1.8.9
|
||||
github.com/nicholas-fedor/shoutrrr v0.10.0
|
||||
github.com/nicholas-fedor/shoutrrr v0.11.1
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.30.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/pocketbase/pocketbase v0.31.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.10
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -35,18 +32,18 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 // 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.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // 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
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
@@ -54,16 +51,16 @@ require (
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/image v0.31.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/image v0.32.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.39.0 // indirect
|
||||
modernc.org/sqlite v1.39.1 // indirect
|
||||
)
|
||||
|
||||
92
go.sum
92
go.sum
@@ -31,8 +31,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -54,8 +54,8 @@ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
|
||||
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -63,26 +63,26 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
||||
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
|
||||
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
|
||||
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.9.1 h1:SEBhM6P1favzILO0f55CY3P9JwvM9RZ7B1ZMCl+Injs=
|
||||
github.com/nicholas-fedor/shoutrrr v0.9.1/go.mod h1:khue5m8LYyMzdPWuJxDTJeT89l9gjwjA+a+r0e8qxxk=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nicholas-fedor/shoutrrr v0.11.1 h1:DND1gW8UM8GYG8c0bUZ5fPFAnm3id8noPdfaFBUmezk=
|
||||
github.com/nicholas-fedor/shoutrrr v0.11.1/go.mod h1:RZuSZSEaSimS47zTOLXb6HJDwLjDHiuJ9SrzxsDcWaQ=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -90,8 +90,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
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.30.1 h1:8lgfhH+HiSw1PyKVMq2sjtC4ZNvda2f/envTAzWMLOA=
|
||||
github.com/pocketbase/pocketbase v0.30.1/go.mod h1:sUI+uekXZam5Wa0eh+DClc+HieKMCeqsHA7Ydd9vwyE=
|
||||
github.com/pocketbase/pocketbase v0.31.0 h1:JaOtSDytdA+a0r4689Mrjda4rmq+BaHgEJkPeOIydms=
|
||||
github.com/pocketbase/pocketbase v0.31.0/go.mod h1:p4a83n+DlBcTvvqhC7QDy0KDmQ2la2c6dgxdIBWwKiE=
|
||||
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -99,8 +99,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
||||
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.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
|
||||
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
@@ -120,25 +120,23 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
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.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -146,23 +144,23 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -179,8 +177,6 @@ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
@@ -191,8 +187,8 @@ 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.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
||||
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
@@ -11,6 +12,12 @@ const (
|
||||
GetData WebSocketAction = iota
|
||||
// Check the fingerprint of the agent
|
||||
CheckFingerprint
|
||||
// Request container logs from agent
|
||||
GetContainerLogs
|
||||
// Request container info from agent
|
||||
GetContainerInfo
|
||||
// Request SMART data from agent
|
||||
GetSmartData
|
||||
// Add new actions here...
|
||||
)
|
||||
|
||||
@@ -23,10 +30,13 @@ type HubRequest[T any] struct {
|
||||
|
||||
// AgentResponse defines the structure for responses sent from agent to hub.
|
||||
type AgentResponse struct {
|
||||
Id *uint32 `cbor:"0,keyasint,omitempty"`
|
||||
SystemData *system.CombinedData `cbor:"1,keyasint,omitempty,omitzero"`
|
||||
Fingerprint *FingerprintResponse `cbor:"2,keyasint,omitempty,omitzero"`
|
||||
Error string `cbor:"3,keyasint,omitempty,omitzero"`
|
||||
Id *uint32 `cbor:"0,keyasint,omitempty"`
|
||||
SystemData *system.CombinedData `cbor:"1,keyasint,omitempty,omitzero"`
|
||||
Fingerprint *FingerprintResponse `cbor:"2,keyasint,omitempty,omitzero"`
|
||||
Error string `cbor:"3,keyasint,omitempty,omitzero"`
|
||||
String *string `cbor:"4,keyasint,omitempty,omitzero"`
|
||||
SmartData map[string]smart.SmartData `cbor:"5,keyasint,omitempty,omitzero"`
|
||||
// Logs *LogsPayload `cbor:"4,keyasint,omitempty,omitzero"`
|
||||
// RawBytes []byte `cbor:"4,keyasint,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
@@ -47,3 +57,11 @@ type DataRequestOptions struct {
|
||||
CacheTimeMs uint16 `cbor:"0,keyasint"`
|
||||
// ResourceType uint8 `cbor:"1,keyasint,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
type ContainerLogsRequest struct {
|
||||
ContainerID string `cbor:"0,keyasint"`
|
||||
}
|
||||
|
||||
type ContainerInfoRequest struct {
|
||||
ContainerID string `cbor:"0,keyasint"`
|
||||
}
|
||||
|
||||
28
internal/dockerfile_agent_alpine
Normal file
28
internal/dockerfile_agent_alpine
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../go.mod ../go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . ./
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
|
||||
# --------------------------
|
||||
# Final image: default scratch-based agent
|
||||
# --------------------------
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
RUN apk add --no-cache smartmontools
|
||||
|
||||
# Ensure data persistence across container recreations
|
||||
VOLUME ["/var/lib/beszel-agent"]
|
||||
|
||||
ENTRYPOINT ["/agent"]
|
||||
@@ -20,7 +20,7 @@ FROM alpine:edge
|
||||
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools
|
||||
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools smartmontools
|
||||
|
||||
# Ensure data persistence across container recreations
|
||||
VOLUME ["/var/lib/beszel-agent"]
|
||||
|
||||
@@ -2,7 +2,6 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
COPY ../go.mod ../go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
@@ -13,7 +12,24 @@ COPY . ./
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
# --------------------------
|
||||
# Smartmontools builder stage
|
||||
# --------------------------
|
||||
FROM nvidia/cuda:12.2.2-base-ubuntu22.04 AS smartmontools-builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
build-essential \
|
||||
&& wget https://downloads.sourceforge.net/project/smartmontools/smartmontools/7.5/smartmontools-7.5.tar.gz \
|
||||
&& tar zxvf smartmontools-7.5.tar.gz \
|
||||
&& cd smartmontools-7.5 \
|
||||
&& ./configure --prefix=/usr --sysconfdir=/etc \
|
||||
&& make \
|
||||
&& make install \
|
||||
&& rm -rf /smartmontools-7.5* \
|
||||
&& apt-get remove -y wget build-essential \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --------------------------
|
||||
# Final image: GPU-enabled agent with nvidia-smi
|
||||
@@ -21,8 +37,8 @@ RUN rm -rf /tmp/*
|
||||
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
# this is so we don't need to create the /tmp directory in the scratch container
|
||||
COPY --from=builder /tmp /tmp
|
||||
# Copy smartmontools binaries and config files
|
||||
COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
|
||||
|
||||
# Ensure data persistence across container recreations
|
||||
VOLUME ["/var/lib/beszel-agent"]
|
||||
|
||||
@@ -8,7 +8,8 @@ type ApiInfo struct {
|
||||
IdShort string
|
||||
Names []string
|
||||
Status string
|
||||
// Image string
|
||||
State string
|
||||
Image string
|
||||
// ImageID string
|
||||
// Command string
|
||||
// Created int64
|
||||
@@ -16,7 +17,6 @@ type ApiInfo struct {
|
||||
// SizeRw int64 `json:",omitempty"`
|
||||
// SizeRootFs int64 `json:",omitempty"`
|
||||
// Labels map[string]string
|
||||
// State string
|
||||
// HostConfig struct {
|
||||
// NetworkMode string `json:",omitempty"`
|
||||
// Annotations map[string]string `json:",omitempty"`
|
||||
@@ -103,6 +103,22 @@ type prevNetStats struct {
|
||||
Recv uint64
|
||||
}
|
||||
|
||||
type DockerHealth = uint8
|
||||
|
||||
const (
|
||||
DockerHealthNone DockerHealth = iota
|
||||
DockerHealthStarting
|
||||
DockerHealthHealthy
|
||||
DockerHealthUnhealthy
|
||||
)
|
||||
|
||||
var DockerHealthStrings = map[string]DockerHealth{
|
||||
"none": DockerHealthNone,
|
||||
"starting": DockerHealthStarting,
|
||||
"healthy": DockerHealthHealthy,
|
||||
"unhealthy": DockerHealthUnhealthy,
|
||||
}
|
||||
|
||||
// Docker container stats
|
||||
type Stats struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
@@ -110,6 +126,11 @@ type Stats struct {
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
NetworkSent float64 `json:"ns" cbor:"3,keyasint"`
|
||||
NetworkRecv float64 `json:"nr" cbor:"4,keyasint"`
|
||||
|
||||
Health DockerHealth `json:"-" cbor:"5,keyasint"`
|
||||
Status string `json:"-" cbor:"6,keyasint"`
|
||||
Id string `json:"-" cbor:"7,keyasint"`
|
||||
Image string `json:"-" cbor:"8,keyasint"`
|
||||
// PrevCpu [2]uint64 `json:"-"`
|
||||
CpuSystem uint64 `json:"-"`
|
||||
CpuContainer uint64 `json:"-"`
|
||||
|
||||
529
internal/entities/smart/smart.go
Normal file
529
internal/entities/smart/smart.go
Normal file
@@ -0,0 +1,529 @@
|
||||
package smart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Common types
|
||||
type VersionInfo [2]int
|
||||
|
||||
type SmartctlInfo struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
SvnRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
InfoName string `json:"info_name"`
|
||||
Type string `json:"type"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
type UserCapacity struct {
|
||||
Blocks uint64 `json:"blocks"`
|
||||
Bytes uint64 `json:"bytes"`
|
||||
}
|
||||
|
||||
// type LocalTime struct {
|
||||
// TimeT int64 `json:"time_t"`
|
||||
// Asctime string `json:"asctime"`
|
||||
// }
|
||||
|
||||
// type WwnInfo struct {
|
||||
// Naa int `json:"naa"`
|
||||
// Oui int `json:"oui"`
|
||||
// ID int `json:"id"`
|
||||
// }
|
||||
|
||||
// type FormFactorInfo struct {
|
||||
// AtaValue int `json:"ata_value"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
|
||||
// type TrimInfo struct {
|
||||
// Supported bool `json:"supported"`
|
||||
// }
|
||||
|
||||
// type AtaVersionInfo struct {
|
||||
// String string `json:"string"`
|
||||
// MajorValue int `json:"major_value"`
|
||||
// MinorValue int `json:"minor_value"`
|
||||
// }
|
||||
|
||||
// type VersionStringInfo struct {
|
||||
// String string `json:"string"`
|
||||
// Value int `json:"value"`
|
||||
// }
|
||||
|
||||
// type SpeedInfo struct {
|
||||
// SataValue int `json:"sata_value"`
|
||||
// String string `json:"string"`
|
||||
// UnitsPerSecond int `json:"units_per_second"`
|
||||
// BitsPerUnit int `json:"bits_per_unit"`
|
||||
// }
|
||||
|
||||
// type InterfaceSpeedInfo struct {
|
||||
// Max SpeedInfo `json:"max"`
|
||||
// Current SpeedInfo `json:"current"`
|
||||
// }
|
||||
|
||||
type SmartStatusInfo struct {
|
||||
Passed bool `json:"passed"`
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
Passed bool `json:"passed"`
|
||||
}
|
||||
|
||||
type PollingMinutes struct {
|
||||
Short int `json:"short"`
|
||||
Extended int `json:"extended"`
|
||||
}
|
||||
|
||||
type CapabilitiesInfo struct {
|
||||
Values []int `json:"values"`
|
||||
ExecOfflineImmediateSupported bool `json:"exec_offline_immediate_supported"`
|
||||
OfflineIsAbortedUponNewCmd bool `json:"offline_is_aborted_upon_new_cmd"`
|
||||
OfflineSurfaceScanSupported bool `json:"offline_surface_scan_supported"`
|
||||
SelfTestsSupported bool `json:"self_tests_supported"`
|
||||
ConveyanceSelfTestSupported bool `json:"conveyance_self_test_supported"`
|
||||
SelectiveSelfTestSupported bool `json:"selective_self_test_supported"`
|
||||
AttributeAutosaveEnabled bool `json:"attribute_autosave_enabled"`
|
||||
ErrorLoggingSupported bool `json:"error_logging_supported"`
|
||||
GpLoggingSupported bool `json:"gp_logging_supported"`
|
||||
}
|
||||
|
||||
// type AtaSmartData struct {
|
||||
// OfflineDataCollection OfflineDataCollectionInfo `json:"offline_data_collection"`
|
||||
// SelfTest SelfTestInfo `json:"self_test"`
|
||||
// Capabilities CapabilitiesInfo `json:"capabilities"`
|
||||
// }
|
||||
|
||||
// type OfflineDataCollectionInfo struct {
|
||||
// Status StatusInfo `json:"status"`
|
||||
// CompletionSeconds int `json:"completion_seconds"`
|
||||
// }
|
||||
|
||||
// type SelfTestInfo struct {
|
||||
// Status StatusInfo `json:"status"`
|
||||
// PollingMinutes PollingMinutes `json:"polling_minutes"`
|
||||
// }
|
||||
|
||||
// type AtaSctCapabilities struct {
|
||||
// Value int `json:"value"`
|
||||
// ErrorRecoveryControlSupported bool `json:"error_recovery_control_supported"`
|
||||
// FeatureControlSupported bool `json:"feature_control_supported"`
|
||||
// DataTableSupported bool `json:"data_table_supported"`
|
||||
// }
|
||||
|
||||
type SummaryInfo struct {
|
||||
Revision int `json:"revision"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type AtaSmartAttributes struct {
|
||||
// Revision int `json:"revision"`
|
||||
Table []AtaSmartAttribute `json:"table"`
|
||||
}
|
||||
|
||||
type AtaSmartAttribute struct {
|
||||
ID uint16 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value uint16 `json:"value"`
|
||||
Worst uint16 `json:"worst"`
|
||||
Thresh uint16 `json:"thresh"`
|
||||
WhenFailed string `json:"when_failed"`
|
||||
// Flags AttributeFlags `json:"flags"`
|
||||
Raw RawValue `json:"raw"`
|
||||
}
|
||||
|
||||
// type AttributeFlags struct {
|
||||
// Value int `json:"value"`
|
||||
// String string `json:"string"`
|
||||
// Prefailure bool `json:"prefailure"`
|
||||
// UpdatedOnline bool `json:"updated_online"`
|
||||
// Performance bool `json:"performance"`
|
||||
// ErrorRate bool `json:"error_rate"`
|
||||
// EventCount bool `json:"event_count"`
|
||||
// AutoKeep bool `json:"auto_keep"`
|
||||
// }
|
||||
|
||||
type RawValue struct {
|
||||
Value SmartRawValue `json:"value"`
|
||||
String string `json:"string"`
|
||||
}
|
||||
|
||||
func (r *RawValue) UnmarshalJSON(data []byte) error {
|
||||
var tmp struct {
|
||||
Value json.RawMessage `json:"value"`
|
||||
String string `json:"string"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tmp.Value) > 0 {
|
||||
if err := r.Value.UnmarshalJSON(tmp.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.Value = 0
|
||||
}
|
||||
|
||||
r.String = tmp.String
|
||||
|
||||
if parsed, ok := ParseSmartRawValueString(tmp.String); ok {
|
||||
r.Value = SmartRawValue(parsed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SmartRawValue uint64
|
||||
|
||||
// handles when drives report strings like "0h+0m+0.000s" or "7344 (253d 8h)" for power on hours
|
||||
func (v *SmartRawValue) UnmarshalJSON(data []byte) error {
|
||||
trimmed := strings.TrimSpace(string(data))
|
||||
if len(trimmed) == 0 || trimmed == "null" {
|
||||
*v = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if trimmed[0] == '"' {
|
||||
valueStr, err := strconv.Unquote(trimmed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, ok := ParseSmartRawValueString(valueStr)
|
||||
if ok {
|
||||
*v = SmartRawValue(parsed)
|
||||
return nil
|
||||
}
|
||||
*v = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if parsed, err := strconv.ParseUint(trimmed, 0, 64); err == nil {
|
||||
*v = SmartRawValue(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
if parsed, ok := ParseSmartRawValueString(trimmed); ok {
|
||||
*v = SmartRawValue(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
*v = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseSmartRawValueString attempts to extract a numeric value from the raw value
|
||||
// strings emitted by smartctl, which sometimes include human-friendly annotations
|
||||
// like "7344 (253d 8h)" or "0h+0m+0.000s". It returns the parsed value and a
|
||||
// boolean indicating success.
|
||||
func ParseSmartRawValueString(value string) (uint64, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if parsed, err := strconv.ParseUint(value, 0, 64); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
if idx := strings.IndexRune(value, 'h'); idx > 0 {
|
||||
hoursPart := strings.TrimSpace(value[:idx])
|
||||
if hoursPart != "" {
|
||||
if parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {
|
||||
return uint64(parsed), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] < '0' || value[i] > '9' {
|
||||
continue
|
||||
}
|
||||
end := i + 1
|
||||
for end < len(value) && value[end] >= '0' && value[end] <= '9' {
|
||||
end++
|
||||
}
|
||||
digits := value[i:end]
|
||||
if parsed, err := strconv.ParseUint(digits, 10, 64); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
i = end
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// type PowerOnTimeInfo struct {
|
||||
// Hours uint32 `json:"hours"`
|
||||
// }
|
||||
|
||||
type TemperatureInfo struct {
|
||||
Current uint8 `json:"current"`
|
||||
}
|
||||
|
||||
type TemperatureInfoScsi struct {
|
||||
Current uint8 `json:"current"`
|
||||
DriveTrip uint8 `json:"drive_trip"`
|
||||
}
|
||||
|
||||
// type SelectiveSelfTestTable struct {
|
||||
// LbaMin int `json:"lba_min"`
|
||||
// LbaMax int `json:"lba_max"`
|
||||
// Status StatusInfo `json:"status"`
|
||||
// }
|
||||
|
||||
// type SelectiveSelfTestFlags struct {
|
||||
// Value int `json:"value"`
|
||||
// RemainderScanEnabled bool `json:"remainder_scan_enabled"`
|
||||
// }
|
||||
|
||||
// type AtaSmartSelectiveSelfTestLog struct {
|
||||
// Revision int `json:"revision"`
|
||||
// Table []SelectiveSelfTestTable `json:"table"`
|
||||
// Flags SelectiveSelfTestFlags `json:"flags"`
|
||||
// PowerUpScanResumeMinutes int `json:"power_up_scan_resume_minutes"`
|
||||
// }
|
||||
|
||||
// BaseSmartInfo contains common fields shared between SATA and NVMe drives
|
||||
// type BaseSmartInfo struct {
|
||||
// Device DeviceInfo `json:"device"`
|
||||
// ModelName string `json:"model_name"`
|
||||
// SerialNumber string `json:"serial_number"`
|
||||
// FirmwareVersion string `json:"firmware_version"`
|
||||
// UserCapacity UserCapacity `json:"user_capacity"`
|
||||
// LogicalBlockSize int `json:"logical_block_size"`
|
||||
// LocalTime LocalTime `json:"local_time"`
|
||||
// }
|
||||
|
||||
type SmartctlInfoLegacy struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
SvnRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
}
|
||||
|
||||
type SmartInfoForSata struct {
|
||||
// JSONFormatVersion VersionInfo `json:"json_format_version"`
|
||||
Smartctl SmartctlInfoLegacy `json:"smartctl"`
|
||||
Device DeviceInfo `json:"device"`
|
||||
// ModelFamily string `json:"model_family"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
// Wwn WwnInfo `json:"wwn"`
|
||||
FirmwareVersion string `json:"firmware_version"`
|
||||
UserCapacity UserCapacity `json:"user_capacity"`
|
||||
ScsiVendor string `json:"scsi_vendor"`
|
||||
ScsiProduct string `json:"scsi_product"`
|
||||
// LogicalBlockSize int `json:"logical_block_size"`
|
||||
// PhysicalBlockSize int `json:"physical_block_size"`
|
||||
// RotationRate int `json:"rotation_rate"`
|
||||
// FormFactor FormFactorInfo `json:"form_factor"`
|
||||
// Trim TrimInfo `json:"trim"`
|
||||
// InSmartctlDatabase bool `json:"in_smartctl_database"`
|
||||
// AtaVersion AtaVersionInfo `json:"ata_version"`
|
||||
// SataVersion VersionStringInfo `json:"sata_version"`
|
||||
// InterfaceSpeed InterfaceSpeedInfo `json:"interface_speed"`
|
||||
// LocalTime LocalTime `json:"local_time"`
|
||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
||||
// AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"`
|
||||
AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"`
|
||||
// PowerOnTime PowerOnTimeInfo `json:"power_on_time"`
|
||||
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||
Temperature TemperatureInfo `json:"temperature"`
|
||||
// AtaSmartErrorLog AtaSmartErrorLog `json:"ata_smart_error_log"`
|
||||
// AtaSmartSelfTestLog AtaSmartSelfTestLog `json:"ata_smart_self_test_log"`
|
||||
// AtaSmartSelectiveSelfTestLog AtaSmartSelectiveSelfTestLog `json:"ata_smart_selective_self_test_log"`
|
||||
}
|
||||
|
||||
type ScsiErrorCounter struct {
|
||||
ErrorsCorrectedByECCFast uint64 `json:"errors_corrected_by_eccfast"`
|
||||
ErrorsCorrectedByECCDelayed uint64 `json:"errors_corrected_by_eccdelayed"`
|
||||
ErrorsCorrectedByRereadsRewrites uint64 `json:"errors_corrected_by_rereads_rewrites"`
|
||||
TotalErrorsCorrected uint64 `json:"total_errors_corrected"`
|
||||
CorrectionAlgorithmInvocations uint64 `json:"correction_algorithm_invocations"`
|
||||
GigabytesProcessed string `json:"gigabytes_processed"`
|
||||
TotalUncorrectedErrors uint64 `json:"total_uncorrected_errors"`
|
||||
}
|
||||
|
||||
type ScsiErrorCounterLog struct {
|
||||
Read ScsiErrorCounter `json:"read"`
|
||||
Write ScsiErrorCounter `json:"write"`
|
||||
Verify ScsiErrorCounter `json:"verify"`
|
||||
}
|
||||
|
||||
type ScsiStartStopCycleCounter struct {
|
||||
YearOfManufacture string `json:"year_of_manufacture"`
|
||||
WeekOfManufacture string `json:"week_of_manufacture"`
|
||||
SpecifiedCycleCountOverDeviceLifetime uint64 `json:"specified_cycle_count_over_device_lifetime"`
|
||||
AccumulatedStartStopCycles uint64 `json:"accumulated_start_stop_cycles"`
|
||||
SpecifiedLoadUnloadCountOverDeviceLifetime uint64 `json:"specified_load_unload_count_over_device_lifetime"`
|
||||
AccumulatedLoadUnloadCycles uint64 `json:"accumulated_load_unload_cycles"`
|
||||
}
|
||||
|
||||
type PowerOnTimeScsi struct {
|
||||
Hours uint64 `json:"hours"`
|
||||
Minutes uint64 `json:"minutes"`
|
||||
}
|
||||
|
||||
type SmartInfoForScsi struct {
|
||||
Smartctl SmartctlInfoLegacy `json:"smartctl"`
|
||||
Device DeviceInfo `json:"device"`
|
||||
ScsiVendor string `json:"scsi_vendor"`
|
||||
ScsiProduct string `json:"scsi_product"`
|
||||
ScsiModelName string `json:"scsi_model_name"`
|
||||
ScsiRevision string `json:"scsi_revision"`
|
||||
ScsiVersion string `json:"scsi_version"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
UserCapacity UserCapacity `json:"user_capacity"`
|
||||
Temperature TemperatureInfoScsi `json:"temperature"`
|
||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||
PowerOnTime PowerOnTimeScsi `json:"power_on_time"`
|
||||
ScsiStartStopCycleCounter ScsiStartStopCycleCounter `json:"scsi_start_stop_cycle_counter"`
|
||||
ScsiGrownDefectList uint64 `json:"scsi_grown_defect_list"`
|
||||
ScsiErrorCounterLog ScsiErrorCounterLog `json:"scsi_error_counter_log"`
|
||||
}
|
||||
|
||||
// type AtaSmartErrorLog struct {
|
||||
// Summary SummaryInfo `json:"summary"`
|
||||
// }
|
||||
|
||||
// type AtaSmartSelfTestLog struct {
|
||||
// Standard SummaryInfo `json:"standard"`
|
||||
// }
|
||||
|
||||
type SmartctlInfoNvme struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
SVNRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
}
|
||||
|
||||
// type NVMePCIVendor struct {
|
||||
// ID int `json:"id"`
|
||||
// SubsystemID int `json:"subsystem_id"`
|
||||
// }
|
||||
|
||||
// type SizeCapacityInfo struct {
|
||||
// Blocks uint64 `json:"blocks"`
|
||||
// Bytes uint64 `json:"bytes"`
|
||||
// }
|
||||
|
||||
// type EUI64Info struct {
|
||||
// OUI int `json:"oui"`
|
||||
// ExtID int `json:"ext_id"`
|
||||
// }
|
||||
|
||||
// type NVMeNamespace struct {
|
||||
// ID uint32 `json:"id"`
|
||||
// Size SizeCapacityInfo `json:"size"`
|
||||
// Capacity SizeCapacityInfo `json:"capacity"`
|
||||
// Utilization SizeCapacityInfo `json:"utilization"`
|
||||
// FormattedLBASize uint32 `json:"formatted_lba_size"`
|
||||
// EUI64 EUI64Info `json:"eui64"`
|
||||
// }
|
||||
|
||||
type SmartStatusInfoNvme struct {
|
||||
Passed bool `json:"passed"`
|
||||
NVMe SmartStatusNVMe `json:"nvme"`
|
||||
}
|
||||
|
||||
type SmartStatusNVMe struct {
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
type NVMeSmartHealthInformationLog struct {
|
||||
CriticalWarning uint `json:"critical_warning"`
|
||||
Temperature uint8 `json:"temperature"`
|
||||
AvailableSpare uint `json:"available_spare"`
|
||||
AvailableSpareThreshold uint `json:"available_spare_threshold"`
|
||||
PercentageUsed uint8 `json:"percentage_used"`
|
||||
DataUnitsRead uint64 `json:"data_units_read"`
|
||||
DataUnitsWritten uint64 `json:"data_units_written"`
|
||||
HostReads uint `json:"host_reads"`
|
||||
HostWrites uint `json:"host_writes"`
|
||||
ControllerBusyTime uint `json:"controller_busy_time"`
|
||||
PowerCycles uint16 `json:"power_cycles"`
|
||||
PowerOnHours uint32 `json:"power_on_hours"`
|
||||
UnsafeShutdowns uint16 `json:"unsafe_shutdowns"`
|
||||
MediaErrors uint `json:"media_errors"`
|
||||
NumErrLogEntries uint `json:"num_err_log_entries"`
|
||||
WarningTempTime uint `json:"warning_temp_time"`
|
||||
CriticalCompTime uint `json:"critical_comp_time"`
|
||||
TemperatureSensors []uint8 `json:"temperature_sensors"`
|
||||
}
|
||||
|
||||
type SmartInfoForNvme struct {
|
||||
// JSONFormatVersion VersionInfo `json:"json_format_version"`
|
||||
Smartctl SmartctlInfoNvme `json:"smartctl"`
|
||||
Device DeviceInfo `json:"device"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
FirmwareVersion string `json:"firmware_version"`
|
||||
// NVMePCIVendor NVMePCIVendor `json:"nvme_pci_vendor"`
|
||||
// NVMeIEEEOUIIdentifier uint32 `json:"nvme_ieee_oui_identifier"`
|
||||
// NVMeTotalCapacity uint64 `json:"nvme_total_capacity"`
|
||||
// NVMeUnallocatedCapacity uint64 `json:"nvme_unallocated_capacity"`
|
||||
// NVMeControllerID uint16 `json:"nvme_controller_id"`
|
||||
// NVMeVersion VersionStringInfo `json:"nvme_version"`
|
||||
// NVMeNumberOfNamespaces uint8 `json:"nvme_number_of_namespaces"`
|
||||
// NVMeNamespaces []NVMeNamespace `json:"nvme_namespaces"`
|
||||
UserCapacity UserCapacity `json:"user_capacity"`
|
||||
// LogicalBlockSize int `json:"logical_block_size"`
|
||||
// LocalTime LocalTime `json:"local_time"`
|
||||
SmartStatus SmartStatusInfoNvme `json:"smart_status"`
|
||||
NVMeSmartHealthInformationLog NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
||||
Temperature TemperatureInfoNvme `json:"temperature"`
|
||||
PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||
PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"`
|
||||
}
|
||||
|
||||
type TemperatureInfoNvme struct {
|
||||
Current int `json:"current"`
|
||||
}
|
||||
|
||||
type PowerOnTimeInfoNvme struct {
|
||||
Hours int `json:"hours"`
|
||||
}
|
||||
|
||||
type SmartData struct {
|
||||
// ModelFamily string `json:"mf,omitempty" cbor:"0,keyasint,omitempty"`
|
||||
ModelName string `json:"mn,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
SerialNumber string `json:"sn,omitempty" cbor:"2,keyasint,omitempty"`
|
||||
FirmwareVersion string `json:"fv,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
Capacity uint64 `json:"c,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
SmartStatus string `json:"s,omitempty" cbor:"5,keyasint,omitempty"`
|
||||
DiskName string `json:"dn,omitempty" cbor:"6,keyasint,omitempty"`
|
||||
DiskType string `json:"dt,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
Temperature uint8 `json:"t,omitempty" cbor:"8,keyasint,omitempty"`
|
||||
Attributes []*SmartAttribute `json:"a,omitempty" cbor:"9,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type SmartAttribute struct {
|
||||
ID uint16 `json:"id,omitempty" cbor:"0,keyasint,omitempty"`
|
||||
Name string `json:"n" cbor:"1,keyasint"`
|
||||
Value uint16 `json:"v,omitempty" cbor:"2,keyasint,omitempty"`
|
||||
Worst uint16 `json:"w,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
Threshold uint16 `json:"t,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
RawValue uint64 `json:"rv" cbor:"5,keyasint"`
|
||||
RawString string `json:"rs,omitempty" cbor:"6,keyasint,omitempty"`
|
||||
WhenFailed string `json:"wf,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
}
|
||||
62
internal/entities/smart/smart_test.go
Normal file
62
internal/entities/smart/smart_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package smart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSmartRawValueUnmarshalDuration(t *testing.T) {
|
||||
input := []byte(`{"value":"62312h+33m+50.907s","string":"62312h+33m+50.907s"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 62312, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalNumericString(t *testing.T) {
|
||||
input := []byte(`{"value":"7344","string":"7344"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 7344, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalParenthetical(t *testing.T) {
|
||||
input := []byte(`{"value":"39925 (212 206 0)","string":"39925 (212 206 0)"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 39925, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalDurationWithFractions(t *testing.T) {
|
||||
input := []byte(`{"value":"2748h+31m+49.560s","string":"2748h+31m+49.560s"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 2748, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalParentheticalRawValue(t *testing.T) {
|
||||
input := []byte(`{"value":57891864217128,"string":"39925 (212 206 0)"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 39925, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalDurationRawValue(t *testing.T) {
|
||||
input := []byte(`{"value":57891864217128,"string":"2748h+31m+49.560s"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 2748, raw.Value)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package system
|
||||
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
@@ -41,9 +42,29 @@ type Stats struct {
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
|
||||
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
|
||||
DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes]
|
||||
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
||||
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
|
||||
DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes]
|
||||
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
||||
CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle]
|
||||
CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..]
|
||||
TopCpuProcess *TopCpuProcess `json:"tcp,omitempty" cbor:"35,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
|
||||
// JSON: encodes as array of numbers (avoids base64 string).
|
||||
// CBOR: falls back to default handling for []uint8 (byte string), keeping payload small.
|
||||
type Uint8Slice []uint8
|
||||
|
||||
func (s Uint8Slice) MarshalJSON() ([]byte, error) {
|
||||
if s == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
// Convert to wider ints to force array-of-numbers encoding.
|
||||
arr := make([]uint16, len(s))
|
||||
for i, v := range s {
|
||||
arr[i] = uint16(v)
|
||||
}
|
||||
return json.Marshal(arr)
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
@@ -62,6 +83,7 @@ type FsStats struct {
|
||||
Time time.Time `json:"-"`
|
||||
Root bool `json:"-"`
|
||||
Mountpoint string `json:"-"`
|
||||
Name string `json:"-"`
|
||||
DiskTotal float64 `json:"d" cbor:"0,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"1,keyasint"`
|
||||
TotalRead uint64 `json:"-"`
|
||||
@@ -132,3 +154,8 @@ type CombinedData struct {
|
||||
Info Info `json:"info" cbor:"1,keyasint"`
|
||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||
}
|
||||
|
||||
type TopCpuProcess struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Percent float64 `json:"p" cbor:"1,keyasint"`
|
||||
}
|
||||
|
||||
@@ -120,18 +120,27 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
|
||||
return err
|
||||
}
|
||||
// set auth settings
|
||||
usersCollection, err := e.App.FindCollectionByNameOrId("users")
|
||||
if err := setCollectionAuthSettings(e.App); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setCollectionAuthSettings sets up default authentication settings for the app
|
||||
func setCollectionAuthSettings(app core.App) error {
|
||||
usersCollection, err := app.FindCollectionByNameOrId("users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
superusersCollection, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
|
||||
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'"
|
||||
@@ -139,29 +148,52 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
|
||||
} else {
|
||||
usersCollection.CreateRule = nil
|
||||
}
|
||||
if err := e.App.Save(usersCollection); err != nil {
|
||||
|
||||
// enable mfaOtp mfa if MFA_OTP env var is set
|
||||
mfaOtp, _ := GetEnv("MFA_OTP")
|
||||
usersCollection.OTP.Length = 6
|
||||
superusersCollection.OTP.Length = 6
|
||||
usersCollection.OTP.Enabled = mfaOtp == "true"
|
||||
usersCollection.MFA.Enabled = mfaOtp == "true"
|
||||
superusersCollection.OTP.Enabled = mfaOtp == "true" || mfaOtp == "superusers"
|
||||
superusersCollection.MFA.Enabled = mfaOtp == "true" || mfaOtp == "superusers"
|
||||
if err := app.Save(superusersCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.Save(usersCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shareAllSystems, _ := GetEnv("SHARE_ALL_SYSTEMS")
|
||||
|
||||
// allow all users to access systems if SHARE_ALL_SYSTEMS is set
|
||||
systemsCollection, err := e.App.FindCachedCollectionByNameOrId("systems")
|
||||
systemsCollection, err := app.FindCollectionByNameOrId("systems")
|
||||
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"
|
||||
var systemsReadRule string
|
||||
if shareAllSystems == "true" {
|
||||
systemsReadRule = "@request.auth.id != \"\""
|
||||
} else {
|
||||
systemsReadRule = "@request.auth.id != \"\" && users.id ?= @request.auth.id"
|
||||
}
|
||||
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 {
|
||||
if err := app.Save(systemsCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
// allow all users to access all containers if SHARE_ALL_SYSTEMS is set
|
||||
containersCollection, err := app.FindCollectionByNameOrId("containers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containersListRule := strings.Replace(systemsReadRule, "users.id", "system.users.id", 1)
|
||||
containersCollection.ListRule = &containersListRule
|
||||
return app.Save(containersCollection)
|
||||
}
|
||||
|
||||
// registerCronJobs sets up scheduled tasks
|
||||
@@ -236,7 +268,15 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
// update / delete user alerts
|
||||
apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts)
|
||||
apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts)
|
||||
|
||||
// get SMART data
|
||||
apiAuth.GET("/smart", h.getSmartData)
|
||||
// /containers routes
|
||||
if enabled, _ := GetEnv("CONTAINER_DETAILS"); enabled != "false" {
|
||||
// get container logs
|
||||
apiAuth.GET("/containers/logs", h.getContainerLogs)
|
||||
// get container info
|
||||
apiAuth.GET("/containers/info", h.getContainerInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -267,6 +307,59 @@ func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// containerRequestHandler handles both container logs and info requests
|
||||
func (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc func(*systems.System, string) (string, error), responseKey string) error {
|
||||
systemID := e.Request.URL.Query().Get("system")
|
||||
containerID := e.Request.URL.Query().Get("container")
|
||||
|
||||
if systemID == "" || containerID == "" {
|
||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and container parameters are required"})
|
||||
}
|
||||
|
||||
system, err := h.sm.GetSystem(systemID)
|
||||
if err != nil {
|
||||
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
|
||||
}
|
||||
|
||||
data, err := fetchFunc(system, containerID)
|
||||
if err != nil {
|
||||
return e.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, map[string]string{responseKey: data})
|
||||
}
|
||||
|
||||
// getContainerLogs handles GET /api/beszel/containers/logs requests
|
||||
func (h *Hub) getContainerLogs(e *core.RequestEvent) error {
|
||||
return h.containerRequestHandler(e, func(system *systems.System, containerID string) (string, error) {
|
||||
return system.FetchContainerLogsFromAgent(containerID)
|
||||
}, "logs")
|
||||
}
|
||||
|
||||
func (h *Hub) getContainerInfo(e *core.RequestEvent) error {
|
||||
return h.containerRequestHandler(e, func(system *systems.System, containerID string) (string, error) {
|
||||
return system.FetchContainerInfoFromAgent(containerID)
|
||||
}, "info")
|
||||
}
|
||||
|
||||
// getSmartData handles GET /api/beszel/smart requests
|
||||
func (h *Hub) getSmartData(e *core.RequestEvent) error {
|
||||
systemID := e.Request.URL.Query().Get("system")
|
||||
if systemID == "" {
|
||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system parameter is required"})
|
||||
}
|
||||
system, err := h.sm.GetSystem(systemID)
|
||||
if err != nil {
|
||||
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
|
||||
}
|
||||
data, err := system.FetchSmartDataFromAgent()
|
||||
if err != nil {
|
||||
return e.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
}
|
||||
e.Response.Header().Set("Cache-Control", "public, max-age=60")
|
||||
return e.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// generates key pair if it doesn't exist and returns signer
|
||||
func (h *Hub) GetSSHKey(dataDir string) (ssh.Signer, error) {
|
||||
if h.signer != nil {
|
||||
|
||||
@@ -449,6 +449,47 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GET /containers/logs - no auth should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/containers/logs?system=test-system&container=test-container",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{"requires valid"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /containers/logs - with auth but missing system param should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/containers/logs?container=test-container",
|
||||
Headers: map[string]string{
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{"system and container parameters are required"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /containers/logs - with auth but missing container param should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/containers/logs?system=test-system",
|
||||
Headers: map[string]string{
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{"system and container parameters are required"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /containers/logs - with auth but invalid system should fail",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/containers/logs?system=invalid-system&container=test-container",
|
||||
Headers: map[string]string{
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{"system not found"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
|
||||
// Auth Optional Routes - Should work without authentication
|
||||
{
|
||||
|
||||
@@ -13,12 +13,14 @@ import (
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -135,41 +137,81 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
|
||||
return nil, err
|
||||
}
|
||||
hub := sys.manager.hub
|
||||
// add system_stats and container_stats records
|
||||
systemStatsCollection, err := hub.FindCachedCollectionByNameOrId("system_stats")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemStatsRecord := core.NewRecord(systemStatsCollection)
|
||||
systemStatsRecord.Set("system", systemRecord.Id)
|
||||
systemStatsRecord.Set("stats", data.Stats)
|
||||
systemStatsRecord.Set("type", "1m")
|
||||
if err := hub.SaveNoValidate(systemStatsRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add new container_stats record
|
||||
if len(data.Containers) > 0 {
|
||||
containerStatsCollection, err := hub.FindCachedCollectionByNameOrId("container_stats")
|
||||
err = hub.RunInTransaction(func(txApp core.App) error {
|
||||
// add system_stats and container_stats records
|
||||
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
containerStatsRecord := core.NewRecord(containerStatsCollection)
|
||||
containerStatsRecord.Set("system", systemRecord.Id)
|
||||
containerStatsRecord.Set("stats", 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", data.Info)
|
||||
if err := hub.SaveNoValidate(systemRecord); err != nil {
|
||||
return nil, err
|
||||
systemStatsRecord := core.NewRecord(systemStatsCollection)
|
||||
systemStatsRecord.Set("system", systemRecord.Id)
|
||||
systemStatsRecord.Set("stats", data.Stats)
|
||||
systemStatsRecord.Set("type", "1m")
|
||||
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data.Containers) > 0 {
|
||||
// add / update containers records
|
||||
if data.Containers[0].Id != "" {
|
||||
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// add new container_stats record
|
||||
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerStatsRecord := core.NewRecord(containerStatsCollection)
|
||||
containerStatsRecord.Set("system", systemRecord.Id)
|
||||
containerStatsRecord.Set("stats", data.Containers)
|
||||
containerStatsRecord.Set("type", "1m")
|
||||
if err := txApp.SaveNoValidate(containerStatsRecord); err != nil {
|
||||
return 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", data.Info)
|
||||
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return systemRecord, err
|
||||
}
|
||||
|
||||
// createContainerRecords creates container records
|
||||
func createContainerRecords(app core.App, data []*container.Stats, systemId string) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return systemRecord, nil
|
||||
params := dbx.Params{
|
||||
"system": systemId,
|
||||
"updated": time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
valueStrings := make([]string, 0, len(data))
|
||||
for i, container := range data {
|
||||
suffix := fmt.Sprintf("%d", i)
|
||||
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:image%[1]s}, {:status%[1]s}, {:health%[1]s}, {:cpu%[1]s}, {:memory%[1]s}, {:net%[1]s}, {:updated})", suffix))
|
||||
params["id"+suffix] = container.Id
|
||||
params["name"+suffix] = container.Name
|
||||
params["image"+suffix] = container.Image
|
||||
params["status"+suffix] = container.Status
|
||||
params["health"+suffix] = container.Health
|
||||
params["cpu"+suffix] = container.Cpu
|
||||
params["memory"+suffix] = container.Mem
|
||||
params["net"+suffix] = container.NetworkSent + container.NetworkRecv
|
||||
}
|
||||
queryString := fmt.Sprintf(
|
||||
"INSERT INTO containers (id, system, name, image, status, health, cpu, memory, net, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system = excluded.system, name = excluded.name, image = excluded.image, status = excluded.status, health = excluded.health, cpu = excluded.cpu, memory = excluded.memory, net = excluded.net, updated = excluded.updated",
|
||||
strings.Join(valueStrings, ","),
|
||||
)
|
||||
_, err := app.DB().NewQuery(queryString).Bind(params).Execute()
|
||||
return err
|
||||
}
|
||||
|
||||
// getRecord retrieves the system record from the database.
|
||||
@@ -242,37 +284,113 @@ func (sys *System) fetchDataViaWebSocket(options common.DataRequestOptions) (*sy
|
||||
return sys.data, nil
|
||||
}
|
||||
|
||||
// fetchStringFromAgentViaSSH is a generic function to fetch strings via SSH
|
||||
func (sys *System) fetchStringFromAgentViaSSH(action common.WebSocketAction, requestData any, errorMsg string) (string, error) {
|
||||
var result string
|
||||
err := sys.runSSHOperation(4*time.Second, 1, func(session *ssh.Session) (bool, error) {
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stdin, stdinErr := session.StdinPipe()
|
||||
if stdinErr != nil {
|
||||
return false, stdinErr
|
||||
}
|
||||
if err := session.Shell(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
req := common.HubRequest[any]{Action: action, Data: requestData}
|
||||
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||
_ = stdin.Close()
|
||||
var resp common.AgentResponse
|
||||
err = cbor.NewDecoder(stdout).Decode(&resp)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp.String == nil {
|
||||
return false, errors.New(errorMsg)
|
||||
}
|
||||
result = *resp.String
|
||||
return false, nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FetchContainerInfoFromAgent fetches container info from the agent
|
||||
func (sys *System) FetchContainerInfoFromAgent(containerID string) (string, error) {
|
||||
// fetch via websocket
|
||||
if sys.WsConn != nil && sys.WsConn.IsConnected() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return sys.WsConn.RequestContainerInfo(ctx, containerID)
|
||||
}
|
||||
// fetch via SSH
|
||||
return sys.fetchStringFromAgentViaSSH(common.GetContainerInfo, common.ContainerInfoRequest{ContainerID: containerID}, "no info in response")
|
||||
}
|
||||
|
||||
// FetchContainerLogsFromAgent fetches container logs from the agent
|
||||
func (sys *System) FetchContainerLogsFromAgent(containerID string) (string, error) {
|
||||
// fetch via websocket
|
||||
if sys.WsConn != nil && sys.WsConn.IsConnected() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return sys.WsConn.RequestContainerLogs(ctx, containerID)
|
||||
}
|
||||
// fetch via SSH
|
||||
return sys.fetchStringFromAgentViaSSH(common.GetContainerLogs, common.ContainerLogsRequest{ContainerID: containerID}, "no logs in response")
|
||||
}
|
||||
|
||||
// FetchSmartDataFromAgent fetches SMART data from the agent
|
||||
func (sys *System) FetchSmartDataFromAgent() (map[string]any, error) {
|
||||
// fetch via websocket
|
||||
if sys.WsConn != nil && sys.WsConn.IsConnected() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return sys.WsConn.RequestSmartData(ctx)
|
||||
}
|
||||
// fetch via SSH
|
||||
var result map[string]any
|
||||
err := sys.runSSHOperation(5*time.Second, 1, func(session *ssh.Session) (bool, error) {
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stdin, stdinErr := session.StdinPipe()
|
||||
if stdinErr != nil {
|
||||
return false, stdinErr
|
||||
}
|
||||
if err := session.Shell(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
req := common.HubRequest[any]{Action: common.GetSmartData}
|
||||
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||
_ = stdin.Close()
|
||||
var resp common.AgentResponse
|
||||
if err := cbor.NewDecoder(stdout).Decode(&resp); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Convert to generic map for JSON response
|
||||
result = make(map[string]any, len(resp.SmartData))
|
||||
for k, v := range resp.SmartData {
|
||||
result[k] = v
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// fetchDataViaSSH handles fetching data using SSH.
|
||||
// This function encapsulates the original SSH logic.
|
||||
// It updates sys.data directly upon successful fetch.
|
||||
func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*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.closeSSHConnection()
|
||||
// Reset format detection on connection failure - agent might have been upgraded
|
||||
continue
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
err := sys.runSSHOperation(4*time.Second, 1, func(session *ssh.Session) (bool, error) {
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
stdin, stdinErr := session.StdinPipe()
|
||||
if err := session.Shell(); err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
*sys.data = system.CombinedData{}
|
||||
@@ -280,45 +398,82 @@ func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.C
|
||||
if sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil {
|
||||
req := common.HubRequest[any]{Action: common.GetData, Data: options}
|
||||
_ = cbor.NewEncoder(stdin).Encode(req)
|
||||
// Close write side to signal end of request
|
||||
_ = stdin.Close()
|
||||
|
||||
var resp common.AgentResponse
|
||||
if decErr := cbor.NewDecoder(stdout).Decode(&resp); decErr == nil && resp.SystemData != nil {
|
||||
*sys.data = *resp.SystemData
|
||||
// wait for the session to complete
|
||||
if err := session.Wait(); err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
return sys.data, nil
|
||||
return false, nil
|
||||
}
|
||||
// If decoding failed, fall back below
|
||||
}
|
||||
|
||||
var decodeErr error
|
||||
if sys.agentVersion.GTE(beszel.MinVersionCbor) {
|
||||
err = cbor.NewDecoder(stdout).Decode(sys.data)
|
||||
decodeErr = cbor.NewDecoder(stdout).Decode(sys.data)
|
||||
} else {
|
||||
err = json.NewDecoder(stdout).Decode(sys.data)
|
||||
decodeErr = json.NewDecoder(stdout).Decode(sys.data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sys.closeSSHConnection()
|
||||
if attempt < maxRetries {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
if decodeErr != nil {
|
||||
return true, decodeErr
|
||||
}
|
||||
|
||||
// wait for the session to complete
|
||||
if err := session.Wait(); err != nil {
|
||||
return nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
return sys.data, nil
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this should never be reached due to the return in the loop
|
||||
return nil, fmt.Errorf("failed to fetch data")
|
||||
return sys.data, nil
|
||||
}
|
||||
|
||||
// runSSHOperation establishes an SSH session and executes the provided operation.
|
||||
// The operation can request a retry by returning true as the first return value.
|
||||
func (sys *System) runSSHOperation(timeout time.Duration, retries int, operation func(*ssh.Session) (bool, error)) error {
|
||||
for attempt := 0; attempt <= retries; attempt++ {
|
||||
if sys.client == nil || sys.Status == down {
|
||||
if err := sys.createSSHClient(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
session, err := sys.createSessionWithTimeout(timeout)
|
||||
if err != nil {
|
||||
if attempt >= retries {
|
||||
return err
|
||||
}
|
||||
sys.manager.hub.Logger().Warn("Session closed. Retrying...", "host", sys.Host, "port", sys.Port, "err", err)
|
||||
sys.closeSSHConnection()
|
||||
continue
|
||||
}
|
||||
|
||||
retry, opErr := func() (bool, error) {
|
||||
defer session.Close()
|
||||
return operation(session)
|
||||
}()
|
||||
|
||||
if opErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if retry {
|
||||
sys.closeSSHConnection()
|
||||
if attempt < retries {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return opErr
|
||||
}
|
||||
|
||||
return fmt.Errorf("ssh operation failed")
|
||||
}
|
||||
|
||||
// createSSHClient creates a new SSH client for the system
|
||||
|
||||
@@ -63,6 +63,15 @@ func NewSystemManager(hub hubLike) *SystemManager {
|
||||
}
|
||||
}
|
||||
|
||||
// GetSystem returns a system by ID from the store
|
||||
func (sm *SystemManager) GetSystem(systemID string) (*System, error) {
|
||||
sys, ok := sm.systems.GetOk(systemID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("system not found")
|
||||
}
|
||||
return sys, nil
|
||||
}
|
||||
|
||||
// Initialize sets up the system manager by binding event hooks and starting existing systems.
|
||||
// It configures SSH client settings and begins monitoring all non-paused systems from the database.
|
||||
// Systems are started with staggered delays to prevent overwhelming the hub during startup.
|
||||
|
||||
@@ -154,19 +154,20 @@ func (sm *SystemManager) startRealtimeWorker() {
|
||||
// fetchRealtimeDataAndNotify fetches realtime data for all active subscriptions and notifies the clients.
|
||||
func (sm *SystemManager) fetchRealtimeDataAndNotify() {
|
||||
for systemId, info := range activeSubscriptions {
|
||||
system, ok := sm.systems.GetOk(systemId)
|
||||
if ok {
|
||||
go func() {
|
||||
data, err := system.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bytes, err := json.Marshal(data)
|
||||
if err == nil {
|
||||
notify(sm.hub, info.subscription, bytes)
|
||||
}
|
||||
}()
|
||||
system, err := sm.GetSystem(systemId)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
data, err := system.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bytes, err := json.Marshal(data)
|
||||
if err == nil {
|
||||
notify(sm.hub, info.subscription, bytes)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ type ResponseHandler interface {
|
||||
}
|
||||
|
||||
// BaseHandler provides a default implementation that can be embedded to make HandleLegacy optional
|
||||
// type BaseHandler struct{}
|
||||
type BaseHandler struct{}
|
||||
|
||||
// func (h *BaseHandler) HandleLegacy(rawData []byte) error {
|
||||
// return errors.New("legacy format not supported")
|
||||
// }
|
||||
func (h *BaseHandler) HandleLegacy(rawData []byte) error {
|
||||
return errors.New("legacy format not supported")
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -63,6 +63,98 @@ func (ws *WsConn) RequestSystemData(ctx context.Context, data *system.CombinedDa
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// stringResponseHandler is a generic handler for string responses from agents
|
||||
type stringResponseHandler struct {
|
||||
BaseHandler
|
||||
value string
|
||||
errorMsg string
|
||||
}
|
||||
|
||||
func (h *stringResponseHandler) Handle(agentResponse common.AgentResponse) error {
|
||||
if agentResponse.String == nil {
|
||||
return errors.New(h.errorMsg)
|
||||
}
|
||||
h.value = *agentResponse.String
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// requestContainerStringViaWS is a generic function to request container-related strings via WebSocket
|
||||
func (ws *WsConn) requestContainerStringViaWS(ctx context.Context, action common.WebSocketAction, requestData any, errorMsg string) (string, error) {
|
||||
if !ws.IsConnected() {
|
||||
return "", gws.ErrConnClosed
|
||||
}
|
||||
|
||||
req, err := ws.requestManager.SendRequest(ctx, action, requestData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
handler := &stringResponseHandler{errorMsg: errorMsg}
|
||||
if err := ws.handleAgentRequest(req, handler); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return handler.value, nil
|
||||
}
|
||||
|
||||
// RequestContainerLogs requests logs for a specific container via WebSocket.
|
||||
func (ws *WsConn) RequestContainerLogs(ctx context.Context, containerID string) (string, error) {
|
||||
return ws.requestContainerStringViaWS(ctx, common.GetContainerLogs, common.ContainerLogsRequest{ContainerID: containerID}, "no logs in response")
|
||||
}
|
||||
|
||||
// RequestContainerInfo requests information about a specific container via WebSocket.
|
||||
func (ws *WsConn) RequestContainerInfo(ctx context.Context, containerID string) (string, error) {
|
||||
return ws.requestContainerStringViaWS(ctx, common.GetContainerInfo, common.ContainerInfoRequest{ContainerID: containerID}, "no info in response")
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// RequestSmartData requests SMART data via WebSocket.
|
||||
func (ws *WsConn) RequestSmartData(ctx context.Context) (map[string]any, error) {
|
||||
if !ws.IsConnected() {
|
||||
return nil, gws.ErrConnClosed
|
||||
}
|
||||
req, err := ws.requestManager.SendRequest(ctx, common.GetSmartData, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]any
|
||||
handler := ResponseHandler(&smartDataHandler{result: &result})
|
||||
if err := ws.handleAgentRequest(req, handler); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// smartDataHandler parses SMART data map from AgentResponse
|
||||
type smartDataHandler struct {
|
||||
BaseHandler
|
||||
result *map[string]any
|
||||
}
|
||||
|
||||
func (h *smartDataHandler) Handle(agentResponse common.AgentResponse) error {
|
||||
if agentResponse.SmartData == nil {
|
||||
return errors.New("no SMART data in response")
|
||||
}
|
||||
// convert to map[string]any for transport convenience in hub layer
|
||||
out := make(map[string]any, len(agentResponse.SmartData))
|
||||
for k, v := range agentResponse.SmartData {
|
||||
out[k] = v
|
||||
}
|
||||
*h.result = out
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// fingerprintHandler implements ResponseHandler for fingerprint requests
|
||||
type fingerprintHandler struct {
|
||||
result *common.FingerprintResponse
|
||||
|
||||
@@ -181,6 +181,17 @@ func TestCommonActions(t *testing.T) {
|
||||
// Test that the actions we use exist and have expected values
|
||||
assert.Equal(t, common.WebSocketAction(0), common.GetData, "GetData should be action 0")
|
||||
assert.Equal(t, common.WebSocketAction(1), common.CheckFingerprint, "CheckFingerprint should be action 1")
|
||||
assert.Equal(t, common.WebSocketAction(2), common.GetContainerLogs, "GetLogs should be action 2")
|
||||
}
|
||||
|
||||
func TestLogsHandler(t *testing.T) {
|
||||
h := &stringResponseHandler{errorMsg: "no logs in response"}
|
||||
|
||||
logValue := "test logs"
|
||||
resp := common.AgentResponse{String: &logValue}
|
||||
err := h.Handle(resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, logValue, h.value)
|
||||
}
|
||||
|
||||
// TestHandler tests that we can create a Handler
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
@@ -719,7 +718,9 @@ func init() {
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"indexes": [
|
||||
"CREATE INDEX ` + "`" + `idx_systems_status` + "`" + ` ON ` + "`" + `systems` + "`" + ` (` + "`" + `status` + "`" + `)"
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
@@ -860,6 +861,152 @@ func init() {
|
||||
"system": false,
|
||||
"authRule": "verified=true",
|
||||
"manageRule": null
|
||||
},
|
||||
{
|
||||
"id": "pbc_1864144027",
|
||||
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"name": "containers",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-f0-9]{6}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 12,
|
||||
"min": 6,
|
||||
"name": "id",
|
||||
"pattern": "^[a-f0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "2hz5ncl8tizk5nx",
|
||||
"hidden": false,
|
||||
"id": "relation3377271179",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "system",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1579384326",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text2063623452",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "status",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number3470402323",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "health",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number3128971310",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"name": "cpu",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number3933025333",
|
||||
"max": null,
|
||||
"min": 0,
|
||||
"name": "memory",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number4075427327",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "net",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number3332085495",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "updated",
|
||||
"onlyInt": true,
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3309110367",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "image",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE INDEX ` + "`" + `idx_JxWirjdhyO` + "`" + ` ON ` + "`" + `containers` + "`" + ` (` + "`" + `updated` + "`" + `)",
|
||||
"CREATE INDEX ` + "`" + `idx_r3Ja0rs102` + "`" + ` ON ` + "`" + `containers` + "`" + ` (` + "`" + `system` + "`" + `)"
|
||||
],
|
||||
"system": false
|
||||
}
|
||||
]`
|
||||
|
||||
@@ -868,31 +1015,6 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all systems that don't have fingerprint records
|
||||
var systemIds []string
|
||||
err = app.DB().NewQuery(`
|
||||
SELECT s.id FROM systems s
|
||||
LEFT JOIN fingerprints f ON s.id = f.system
|
||||
WHERE f.system IS NULL
|
||||
`).Column(&systemIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create fingerprint records with unique UUID tokens for each system
|
||||
for _, systemId := range systemIds {
|
||||
token := uuid.New().String()
|
||||
_, err = app.DB().NewQuery(`
|
||||
INSERT INTO fingerprints (system, token)
|
||||
VALUES ({:system}, {:token})
|
||||
`).Bind(map[string]any{
|
||||
"system": systemId,
|
||||
"token": token,
|
||||
}).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
return nil
|
||||
@@ -177,6 +177,10 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
stats := &tempStats
|
||||
// necessary because uint8 is not big enough for the sum
|
||||
batterySum := 0
|
||||
// accumulate per-core usage across records
|
||||
var cpuCoresSums []uint64
|
||||
// accumulate cpu breakdown [user, system, iowait, steal, idle]
|
||||
var cpuBreakdownSums []float64
|
||||
|
||||
count := float64(len(records))
|
||||
tempCount := float64(0)
|
||||
@@ -194,6 +198,15 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
}
|
||||
|
||||
sum.Cpu += stats.Cpu
|
||||
// accumulate cpu time breakdowns if present
|
||||
if stats.CpuBreakdown != nil {
|
||||
if len(cpuBreakdownSums) < len(stats.CpuBreakdown) {
|
||||
cpuBreakdownSums = append(cpuBreakdownSums, make([]float64, len(stats.CpuBreakdown)-len(cpuBreakdownSums))...)
|
||||
}
|
||||
for i, v := range stats.CpuBreakdown {
|
||||
cpuBreakdownSums[i] += v
|
||||
}
|
||||
}
|
||||
sum.Mem += stats.Mem
|
||||
sum.MemUsed += stats.MemUsed
|
||||
sum.MemPct += stats.MemPct
|
||||
@@ -217,6 +230,17 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.DiskIO[1] += stats.DiskIO[1]
|
||||
batterySum += int(stats.Battery[0])
|
||||
sum.Battery[1] = stats.Battery[1]
|
||||
|
||||
// accumulate per-core usage if present
|
||||
if stats.CpuCoresUsage != nil {
|
||||
if len(cpuCoresSums) < len(stats.CpuCoresUsage) {
|
||||
// extend slices to accommodate core count
|
||||
cpuCoresSums = append(cpuCoresSums, make([]uint64, len(stats.CpuCoresUsage)-len(cpuCoresSums))...)
|
||||
}
|
||||
for i, v := range stats.CpuCoresUsage {
|
||||
cpuCoresSums[i] += uint64(v)
|
||||
}
|
||||
}
|
||||
// Set peak values
|
||||
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
|
||||
@@ -269,6 +293,10 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
fs.DiskReadPs += value.DiskReadPs
|
||||
fs.MaxDiskReadPS = max(fs.MaxDiskReadPS, value.MaxDiskReadPS, value.DiskReadPs)
|
||||
fs.MaxDiskWritePS = max(fs.MaxDiskWritePS, value.MaxDiskWritePS, value.DiskWritePs)
|
||||
fs.DiskReadBytes += value.DiskReadBytes
|
||||
fs.DiskWriteBytes += value.DiskWriteBytes
|
||||
fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)
|
||||
fs.MaxDiskWriteBytes = max(fs.MaxDiskWriteBytes, value.MaxDiskWriteBytes, value.DiskWriteBytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +384,8 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
fs.DiskUsed = twoDecimals(fs.DiskUsed / count)
|
||||
fs.DiskWritePs = twoDecimals(fs.DiskWritePs / count)
|
||||
fs.DiskReadPs = twoDecimals(fs.DiskReadPs / count)
|
||||
fs.DiskReadBytes = fs.DiskReadBytes / uint64(count)
|
||||
fs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +409,25 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.GPUData[id] = gpu
|
||||
}
|
||||
}
|
||||
|
||||
// Average per-core usage
|
||||
if len(cpuCoresSums) > 0 {
|
||||
avg := make(system.Uint8Slice, len(cpuCoresSums))
|
||||
for i := range cpuCoresSums {
|
||||
v := math.Round(float64(cpuCoresSums[i]) / count)
|
||||
avg[i] = uint8(v)
|
||||
}
|
||||
sum.CpuCoresUsage = avg
|
||||
}
|
||||
|
||||
// Average CPU breakdown
|
||||
if len(cpuBreakdownSums) > 0 {
|
||||
avg := make([]float64, len(cpuBreakdownSums))
|
||||
for i := range cpuBreakdownSums {
|
||||
avg[i] = twoDecimals(cpuBreakdownSums[i] / count)
|
||||
}
|
||||
sum.CpuBreakdown = avg
|
||||
}
|
||||
}
|
||||
|
||||
return sum
|
||||
@@ -437,6 +486,10 @@ func (rm *RecordManager) DeleteOldRecords() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = deleteOldContainerRecords(txApp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = deleteOldAlertsHistory(txApp, 200, 250)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -506,6 +559,20 @@ func deleteOldSystemStats(app core.App) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deletes container records that haven't been updated in the last 10 minutes
|
||||
func deleteOldContainerRecords(app core.App) error {
|
||||
now := time.Now().UTC()
|
||||
tenMinutesAgo := now.Add(-10 * time.Minute)
|
||||
|
||||
// Delete container records where updated < tenMinutesAgo
|
||||
_, err := app.DB().NewQuery("DELETE FROM containers WHERE updated < {:updated}").Bind(dbx.Params{"updated": tenMinutesAgo.UnixMilli()}).Execute()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete old container records: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/* Round float to two decimals */
|
||||
func twoDecimals(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="manifest" href="./static/manifest.json" />
|
||||
<link rel="icon" type="image/svg+xml" href="./static/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="./static/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>Beszel</title>
|
||||
|
||||
@@ -11,10 +11,10 @@ export default defineConfig({
|
||||
"es",
|
||||
"fa",
|
||||
"fr",
|
||||
"he",
|
||||
"hr",
|
||||
"hu",
|
||||
"it",
|
||||
"is",
|
||||
"ja",
|
||||
"ko",
|
||||
"nl",
|
||||
|
||||
761
internal/site/package-lock.json
generated
761
internal/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.13.1",
|
||||
"version": "0.15.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -49,11 +49,12 @@
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"recharts": "^2.15.4",
|
||||
"shiki": "^3.13.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"valibot": "^0.42.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.3",
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@lingui/cli": "^5.4.1",
|
||||
"@lingui/swc-plugin": "^5.6.1",
|
||||
"@lingui/vite-plugin": "^5.4.1",
|
||||
@@ -76,4 +77,4 @@
|
||||
"optionalDependencies": {
|
||||
"@esbuild/linux-arm64": "^0.21.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 70" fill="#22c55e"><path d="M35 70H0V0h35q4.4 0 8.2 1.7a21.4 21.4 0 0 1 6.6 4.5q2.9 2.8 4.5 6.6Q56 16.7 56 21a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5Q53 39.1 54 41a18.3 18.3 0 0 1 1.5 4 17.4 17.4 0 0 1 .5 3 15.3 15.3 0 0 1 0 1q0 4.4-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.6Q39.4 70 35 70ZM14 14v14h21a7 7 0 0 0 2.3-.3 6.6 6.6 0 0 0 .4-.2Q39 27 40 26a6.9 6.9 0 0 0 1.5-2.2q.5-1.3.5-2.8a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 17 40 16a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm0 28v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 54.9 40 54a7 7 0 0 0 1.5-2.2 6.9 6.9 0 0 0 .5-2.6 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 45 40 44a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 906 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 70" fill="#dc2626"><path d="M35 70H0V0h35q4.4 0 8.2 1.7a21.4 21.4 0 0 1 6.6 4.5q2.9 2.8 4.5 6.6Q56 16.7 56 21a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5Q53 39.1 54 41a18.3 18.3 0 0 1 1.5 4 17.4 17.4 0 0 1 .5 3 15.3 15.3 0 0 1 0 1q0 4.4-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.6Q39.4 70 35 70ZM14 14v14h21a7 7 0 0 0 2.3-.3 6.6 6.6 0 0 0 .4-.2Q39 27 40 26a6.9 6.9 0 0 0 1.5-2.2q.5-1.3.5-2.8a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 17 40 16a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm0 28v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 54.9 40 54a7 7 0 0 0 1.5-2.2 6.9 6.9 0 0 0 .5-2.6 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 45 40 44a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 906 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 70" fill="#888"><path d="M35 70H0V0h35q4.4 0 8.2 1.7a21.4 21.4 0 0 1 6.6 4.5q2.9 2.8 4.5 6.6Q56 16.7 56 21a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5Q53 39.1 54 41a18.3 18.3 0 0 1 1.5 4 17.4 17.4 0 0 1 .5 3 15.3 15.3 0 0 1 0 1q0 4.4-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.6Q39.4 70 35 70ZM14 14v14h21a7 7 0 0 0 2.3-.3 6.6 6.6 0 0 0 .4-.2Q39 27 40 26a6.9 6.9 0 0 0 1.5-2.2q.5-1.3.5-2.8a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 17 40 16a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm0 28v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 54.9 40 54a7 7 0 0 0 1.5-2.2 6.9 6.9 0 0 0 .5-2.6 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 45 40 44a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 903 B |
9
internal/site/public/static/icon.svg
Normal file
9
internal/site/public/static/icon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 70">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="20%" x2="100%" y2="120%">
|
||||
<stop offset="0%" style="stop-color:#747bff"/>
|
||||
<stop offset="100%" style="stop-color:#24eb5c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#gradient)" d="M35 70H0V0h35q4.4 0 8.2 1.7a21.4 21.4 0 0 1 6.6 4.5q2.9 2.8 4.5 6.6Q56 16.7 56 21a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5Q53 39.1 54 41a18.3 18.3 0 0 1 1.5 4 17.4 17.4 0 0 1 .5 3 15.3 15.3 0 0 1 0 1q0 4.4-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.6Q39.4 70 35 70ZM14 14v14h21a7 7 0 0 0 2.3-.3 6.6 6.6 0 0 0 .4-.2Q39 27 40 26a6.9 6.9 0 0 0 1.5-2.2q.5-1.3.5-2.8a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 17 40 16a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm0 28v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 54.9 40 54a7 7 0 0 0 1.5-2.2 6.9 6.9 0 0 0 .5-2.6 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 45 40 44a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
85
internal/site/src/components/active-alerts.tsx
Normal file
85
internal/site/src/components/active-alerts.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { $alerts, $allSystemsById } from "@/lib/stores"
|
||||
import type { AlertRecord } from "@/types"
|
||||
import { Plural, Trans } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { useMemo } from "react"
|
||||
import { $router, Link } from "./router"
|
||||
import { Alert, AlertTitle, AlertDescription } from "./ui/alert"
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "./ui/card"
|
||||
|
||||
export const ActiveAlerts = () => {
|
||||
const alerts = useStore($alerts)
|
||||
const systems = useStore($allSystemsById)
|
||||
|
||||
const { activeAlerts, alertsKey } = useMemo(() => {
|
||||
const activeAlerts: AlertRecord[] = []
|
||||
// key to prevent re-rendering if alerts change but active alerts didn't
|
||||
const alertsKey: string[] = []
|
||||
|
||||
for (const systemId of Object.keys(alerts)) {
|
||||
for (const alert of alerts[systemId].values()) {
|
||||
if (alert.triggered && alert.name in alertInfo) {
|
||||
activeAlerts.push(alert)
|
||||
alertsKey.push(`${alert.system}${alert.value}${alert.min}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { activeAlerts, alertsKey }
|
||||
}, [alerts])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: alertsKey is inclusive
|
||||
return useMemo(() => {
|
||||
if (activeAlerts.length === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
<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-px duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black/5"
|
||||
>
|
||||
<info.icon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{alert.name === "Status" ? (
|
||||
<Trans>Connection is down</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
|
||||
</Trans>
|
||||
)}
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { id: systems[alert.system]?.id })}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
aria-label="View system"
|
||||
></Link>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}, [alertsKey.join("")])
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
|
||||
/>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="max-h-full overflow-auto w-145 !max-w-full p-4 sm:p-6">
|
||||
<SheetContent className="max-h-full overflow-auto w-150 !max-w-full p-4 sm:p-6">
|
||||
{opened && <AlertDialogContent system={system} />}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -17,6 +17,7 @@ export type DataPoint = {
|
||||
dataKey: (data: SystemStatsRecord) => number | undefined
|
||||
color: number | string
|
||||
opacity: number
|
||||
stackId?: string | number
|
||||
}
|
||||
|
||||
export default function AreaChartDefault({
|
||||
@@ -29,19 +30,23 @@ export default function AreaChartDefault({
|
||||
domain,
|
||||
legend,
|
||||
itemSorter,
|
||||
reverseStackOrder = false,
|
||||
hideYAxis = false,
|
||||
}: // logRender = false,
|
||||
{
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
maxToggled?: boolean
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
domain?: [number, number]
|
||||
legend?: boolean
|
||||
itemSorter?: (a: any, b: any) => number
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
{
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
maxToggled?: boolean
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
domain?: [number, number]
|
||||
legend?: boolean
|
||||
itemSorter?: (a: any, b: any) => number
|
||||
reverseStackOrder?: boolean
|
||||
hideYAxis?: boolean
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
|
||||
@@ -56,12 +61,13 @@ export default function AreaChartDefault({
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
"opacity-100": yAxisWidth || hideYAxis,
|
||||
"ps-4": hideYAxis,
|
||||
})}
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
<AreaChart reverseStackOrder={reverseStackOrder} accessibilityLayer data={chartData.systemStats} margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
{!hideYAxis && <YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
@@ -70,7 +76,7 @@ export default function AreaChartDefault({
|
||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
/>}
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
@@ -99,10 +105,11 @@ export default function AreaChartDefault({
|
||||
fillOpacity={dataPoint.opacity}
|
||||
stroke={color}
|
||||
isAnimationActive={false}
|
||||
stackId={dataPoint.stackId}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{legend && <ChartLegend content={<ChartLegendContent />} />}
|
||||
{legend && <ChartLegend content={<ChartLegendContent reverse={reverseStackOrder} />} />}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
|
||||
@@ -94,8 +94,11 @@ export default memo(function ContainerChart({
|
||||
if (!filter) {
|
||||
return new Set<string>()
|
||||
}
|
||||
const filterLower = filter.toLowerCase()
|
||||
return new Set(Object.keys(chartConfig).filter((key) => !key.toLowerCase().includes(filterLower)))
|
||||
const filterTerms = filter.toLowerCase().split(" ").filter(term => term.length > 0)
|
||||
return new Set(Object.keys(chartConfig).filter((key) => {
|
||||
const keyLower = key.toLowerCase()
|
||||
return !filterTerms.some(term => keyLower.includes(term))
|
||||
}))
|
||||
}, [chartConfig, filter])
|
||||
|
||||
// console.log('rendered at', new Date())
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { Unit } from "@/lib/enums"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData } from "@/types"
|
||||
import type { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function DiskChart({
|
||||
@@ -12,7 +12,7 @@ export default memo(function DiskChart({
|
||||
diskSize,
|
||||
chartData,
|
||||
}: {
|
||||
dataKey: string
|
||||
dataKey: string | ((data: SystemStatsRecord) => number | undefined)
|
||||
diskSize: number
|
||||
chartData: ChartData
|
||||
}) {
|
||||
|
||||
@@ -69,7 +69,7 @@ export function useContainerChartConfigs(containerData: ChartData["containerData
|
||||
const hue = ((i * 360) / count) % 360
|
||||
chartConfig[containerName] = {
|
||||
label: containerName,
|
||||
color: `hsl(${hue}, 60%, 55%)`,
|
||||
color: `hsl(${hue}, var(--chart-saturation), var(--chart-lightness))`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
domain={[0, "auto"]}
|
||||
domain={["auto", "auto"]}
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
|
||||
@@ -91,7 +91,8 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
}
|
||||
/>
|
||||
{colors.map((key) => {
|
||||
const filtered = filter && !key.toLowerCase().includes(filter.toLowerCase())
|
||||
const filterTerms = filter ? filter.toLowerCase().split(" ").filter(term => term.length > 0) : []
|
||||
const filtered = filterTerms.length > 0 && !filterTerms.some(term => key.toLowerCase().includes(term))
|
||||
const strokeOpacity = filtered ? 0.1 : 1
|
||||
return (
|
||||
<Line
|
||||
@@ -113,4 +114,4 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { DialogDescription } from "@radix-ui/react-dialog"
|
||||
import {
|
||||
AlertOctagonIcon,
|
||||
BookIcon,
|
||||
ContainerIcon,
|
||||
DatabaseBackupIcon,
|
||||
FingerprintIcon,
|
||||
LayoutDashboard,
|
||||
@@ -80,7 +81,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
)}
|
||||
<CommandGroup heading={t`Pages / Settings`}>
|
||||
<CommandItem
|
||||
keywords={["home"]}
|
||||
keywords={["home", t`All Systems`]}
|
||||
onSelect={() => {
|
||||
navigate(basePath)
|
||||
setOpen(false)
|
||||
@@ -94,6 +95,20 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
<Trans>Page</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "containers"))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<ContainerIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>All Containers</Trans>
|
||||
</span>
|
||||
<CommandShortcut>
|
||||
<Trans>Page</Trans>
|
||||
</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "settings", { name: "general" }))
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { Column, ColumnDef } from "@tanstack/react-table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn, decimalString, formatBytes, hourWithSeconds } from "@/lib/utils"
|
||||
import type { ContainerRecord } from "@/types"
|
||||
import { ContainerHealth, ContainerHealthLabels } from "@/lib/enums"
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
ClockIcon,
|
||||
ContainerIcon,
|
||||
CpuIcon,
|
||||
LayersIcon,
|
||||
MemoryStickIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "lucide-react"
|
||||
import { EthernetIcon, HourglassIcon } from "../ui/icons"
|
||||
import { Badge } from "../ui/badge"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { $allSystemsById } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
|
||||
// Unit names and their corresponding number of seconds for converting docker status strings
|
||||
const unitSeconds = [["s", 1], ["mi", 60], ["h", 3600], ["d", 86400], ["w", 604800], ["mo", 2592000]] as const
|
||||
// Convert docker status string to number of seconds ("Up X minutes", "Up X hours", etc.)
|
||||
function getStatusValue(status: string): number {
|
||||
const [_, num, unit] = status.split(" ")
|
||||
const numValue = Number(num)
|
||||
for (const [unitName, value] of unitSeconds) {
|
||||
if (unit.startsWith(unitName)) {
|
||||
return numValue * value
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
{
|
||||
id: "name",
|
||||
sortingFn: (a, b) => a.original.name.localeCompare(b.original.name),
|
||||
accessorFn: (record) => record.name,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={ContainerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 xl:w-48 block truncate">{getValue() as string}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "system",
|
||||
accessorFn: (record) => record.system,
|
||||
sortingFn: (a, b) => {
|
||||
const allSystems = $allSystemsById.get()
|
||||
const systemNameA = allSystems[a.original.system]?.name ?? ""
|
||||
const systemNameB = allSystems[b.original.system]?.name ?? ""
|
||||
return systemNameA.localeCompare(systemNameB)
|
||||
},
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const allSystems = useStore($allSystemsById)
|
||||
return <span className="ms-1.5 xl:w-34 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: "id",
|
||||
// accessorFn: (record) => record.id,
|
||||
// sortingFn: (a, b) => a.original.id.localeCompare(b.original.id),
|
||||
// header: ({ column }) => <HeaderButton column={column} name="ID" Icon={HashIcon} />,
|
||||
// cell: ({ getValue }) => {
|
||||
// return <span className="ms-1.5 me-3 font-mono">{getValue() as string}</span>
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: "cpu",
|
||||
accessorFn: (record) => record.cpu,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
return <span className="ms-1.5 tabular-nums">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
accessorFn: (record) => record.memory,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Memory`} Icon={MemoryStickIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, false, undefined, true)
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "net",
|
||||
accessorFn: (record) => record.net,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Net`} Icon={EthernetIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, true, undefined, true)
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "health",
|
||||
invertSorting: true,
|
||||
accessorFn: (record) => record.health,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Health`} Icon={ShieldCheckIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const healthValue = getValue() as number
|
||||
const healthStatus = ContainerHealthLabels[healthValue] || "Unknown"
|
||||
return (
|
||||
<Badge variant="outline" className="dark:border-white/12">
|
||||
<span className={cn("size-2 me-1.5 rounded-full", {
|
||||
"bg-green-500": healthValue === ContainerHealth.Healthy,
|
||||
"bg-red-500": healthValue === ContainerHealth.Unhealthy,
|
||||
"bg-yellow-500": healthValue === ContainerHealth.Starting,
|
||||
"bg-zinc-500": healthValue === ContainerHealth.None,
|
||||
})}>
|
||||
</span>
|
||||
{healthStatus}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "image",
|
||||
sortingFn: (a, b) => a.original.image.localeCompare(b.original.image),
|
||||
accessorFn: (record) => record.image,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t({ message: "Image", context: "Docker image" })} Icon={LayersIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 xl:w-40 block truncate">{getValue() as string}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
accessorFn: (record) => record.status,
|
||||
invertSorting: true,
|
||||
sortingFn: (a, b) => getStatusValue(a.original.status) - getStatusValue(b.original.status),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={HourglassIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 w-25 block truncate">{getValue() as string}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "updated",
|
||||
invertSorting: true,
|
||||
accessorFn: (record) => record.updated,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const timestamp = getValue() as number
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">
|
||||
{hourWithSeconds(new Date(timestamp).toISOString())}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function HeaderButton({ column, name, Icon }: { column: Column<ContainerRecord>; name: string; Icon: React.ElementType }) {
|
||||
const isSorted = column.getIsSorted()
|
||||
return (
|
||||
<Button
|
||||
className={cn("h-9 px-3 flex items-center gap-2 duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{Icon && <Icon className="size-4" />}
|
||||
{name}
|
||||
<ArrowUpDownIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import {
|
||||
type ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type Table as TableType,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual"
|
||||
import { memo, RefObject, useEffect, useRef, useState } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { pb } from "@/lib/api"
|
||||
import type { ContainerRecord } from "@/types"
|
||||
import { containerChartCols } from "@/components/containers-table/containers-table-columns"
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { type ContainerHealth, ContainerHealthLabels } from "@/lib/enums"
|
||||
import { cn, useBrowserStorage } from "@/lib/utils"
|
||||
import { Sheet, SheetTitle, SheetHeader, SheetContent, SheetDescription } from "../ui/sheet"
|
||||
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { $allSystemsById } from "@/lib/stores"
|
||||
import { MaximizeIcon, RefreshCwIcon } from "lucide-react"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { $router, Link } from "../router"
|
||||
import { listenKeys } from "nanostores"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const syntaxTheme = "github-dark-dimmed"
|
||||
|
||||
export default function ContainersTable({ systemId }: { systemId?: string }) {
|
||||
const [data, setData] = useState<ContainerRecord[]>([])
|
||||
const [sorting, setSorting] = useBrowserStorage<SortingState>(
|
||||
`sort-c-${systemId ? 1 : 0}`,
|
||||
[{ id: systemId ? "name" : "system", desc: false }],
|
||||
sessionStorage
|
||||
)
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [globalFilter, setGlobalFilter] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const pbOptions = {
|
||||
fields: "id,name,image,cpu,memory,net,health,status,system,updated",
|
||||
}
|
||||
|
||||
const fetchData = (lastXMs: number) => {
|
||||
const updated = Date.now() - lastXMs
|
||||
let filter: string
|
||||
if (systemId) {
|
||||
filter = pb.filter("system={:system} && updated > {:updated}", { system: systemId, updated })
|
||||
} else {
|
||||
filter = pb.filter("updated > {:updated}", { updated })
|
||||
}
|
||||
pb.collection<ContainerRecord>("containers")
|
||||
.getList(0, 2000, {
|
||||
...pbOptions,
|
||||
filter,
|
||||
})
|
||||
.then(({ items }) => setData((curItems) => {
|
||||
const containerIds = new Set(items.map(item => item.id))
|
||||
const now = Date.now()
|
||||
for (const item of curItems) {
|
||||
if (!containerIds.has(item.id) && now - item.updated < 70_000) {
|
||||
items.push(item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}))
|
||||
}
|
||||
|
||||
// initial load
|
||||
fetchData(70_000)
|
||||
|
||||
// if no systemId, poll every 10 seconds
|
||||
if (!systemId) {
|
||||
// poll every 10 seconds
|
||||
const intervalId = setInterval(() => fetchData(10_500), 10_000)
|
||||
// clear interval on unmount
|
||||
return () => clearInterval(intervalId)
|
||||
}
|
||||
|
||||
// if systemId, fetch containers after the system is updated
|
||||
return listenKeys($allSystemsById, [systemId], (_newSystems) => {
|
||||
const changeTime = Date.now()
|
||||
setTimeout(() => fetchData(Date.now() - changeTime + 1000), 100)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: containerChartCols.filter(col => systemId ? col.id !== "system" : true),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
defaultColumn: {
|
||||
sortUndefined: "last",
|
||||
size: 100,
|
||||
minSize: 0,
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
globalFilter,
|
||||
},
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
globalFilterFn: (row, _columnId, filterValue) => {
|
||||
const container = row.original
|
||||
const systemName = $allSystemsById.get()[container.system]?.name ?? ""
|
||||
const id = container.id ?? ""
|
||||
const name = container.name ?? ""
|
||||
const status = container.status ?? ""
|
||||
const healthLabel = ContainerHealthLabels[container.health as ContainerHealth] ?? ""
|
||||
const image = container.image ?? ""
|
||||
const searchString = `${systemName} ${id} ${name} ${healthLabel} ${status} ${image}`.toLowerCase()
|
||||
|
||||
return (filterValue as string)
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.every((term) => searchString.includes(term))
|
||||
},
|
||||
})
|
||||
|
||||
const rows = table.getRowModel().rows
|
||||
const visibleColumns = table.getVisibleLeafColumns()
|
||||
|
||||
return (
|
||||
<Card className="p-6 @container w-full">
|
||||
<CardHeader className="p-0 mb-4">
|
||||
<div className="grid md:flex gap-5 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2">
|
||||
<Trans>All Containers</Trans>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex">
|
||||
<Trans>Click on a container to view more information.</Trans>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t`Filter...`}
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="ms-auto px-4 w-full max-w-full md:w-64"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className="rounded-md">
|
||||
<AllContainersTable table={table} rows={rows} colLength={visibleColumns.length} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const AllContainersTable = memo(
|
||||
function AllContainersTable({ table, rows, colLength }: { table: TableType<ContainerRecord>; rows: Row<ContainerRecord>[]; colLength: number }) {
|
||||
// The virtualizer will need a reference to the scrollable container element
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const activeContainer = useRef<ContainerRecord | null>(null)
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
const openSheet = (container: ContainerRecord) => {
|
||||
activeContainer.current = container
|
||||
setSheetOpen(true)
|
||||
}
|
||||
|
||||
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
||||
count: rows.length,
|
||||
estimateSize: () => 54,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 5,
|
||||
})
|
||||
const virtualRows = virtualizer.getVirtualItems()
|
||||
|
||||
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
|
||||
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
|
||||
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
|
||||
(!rows.length || rows.length > 2) && "min-h-50"
|
||||
)}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{/* add header height to table size */}
|
||||
<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>
|
||||
<table className="text-sm w-full h-full text-nowrap">
|
||||
<ContainersTableHead table={table} />
|
||||
<TableBody>
|
||||
{rows.length ? (
|
||||
virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index]
|
||||
return (
|
||||
<ContainerTableRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
virtualRow={virtualRow}
|
||||
openSheet={openSheet}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
|
||||
<Trans>No results.</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
<ContainerSheet sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} activeContainer={activeContainer} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async function getLogsHtml(container: ContainerRecord): Promise<string> {
|
||||
try {
|
||||
const [{ highlighter }, logsHtml] = await Promise.all([import('@/lib/shiki'), pb.send<{ logs: string }>("/api/beszel/containers/logs", {
|
||||
system: container.system,
|
||||
container: container.id,
|
||||
})])
|
||||
return logsHtml.logs ? highlighter.codeToHtml(logsHtml.logs, { lang: "log", theme: syntaxTheme }) : t`No results.`
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
async function getInfoHtml(container: ContainerRecord): Promise<string> {
|
||||
try {
|
||||
let [{ highlighter }, { info }] = await Promise.all([import('@/lib/shiki'), pb.send<{ info: string }>("/api/beszel/containers/info", {
|
||||
system: container.system,
|
||||
container: container.id,
|
||||
})])
|
||||
try {
|
||||
info = JSON.stringify(JSON.parse(info), null, 2)
|
||||
} catch (_) { }
|
||||
return info ? highlighter.codeToHtml(info, { lang: "json", theme: syntaxTheme }) : t`No results.`
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function ContainerSheet({ sheetOpen, setSheetOpen, activeContainer }: { sheetOpen: boolean, setSheetOpen: (open: boolean) => void, activeContainer: RefObject<ContainerRecord | null> }) {
|
||||
const container = activeContainer.current
|
||||
if (!container) return null
|
||||
|
||||
const [logsDisplay, setLogsDisplay] = useState<string>("")
|
||||
const [infoDisplay, setInfoDisplay] = useState<string>("")
|
||||
const [logsFullscreenOpen, setLogsFullscreenOpen] = useState<boolean>(false)
|
||||
const [infoFullscreenOpen, setInfoFullscreenOpen] = useState<boolean>(false)
|
||||
const [isRefreshingLogs, setIsRefreshingLogs] = useState<boolean>(false)
|
||||
const logsContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function scrollLogsToBottom() {
|
||||
if (logsContainerRef.current) {
|
||||
logsContainerRef.current.scrollTo({ top: logsContainerRef.current.scrollHeight })
|
||||
}
|
||||
}
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsRefreshingLogs(true)
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const logsHtml = await getLogsHtml(container)
|
||||
setLogsDisplay(logsHtml)
|
||||
setTimeout(scrollLogsToBottom, 20)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
// Ensure minimum spin duration of 800ms
|
||||
const elapsed = Date.now() - startTime
|
||||
const remaining = Math.max(0, 500 - elapsed)
|
||||
setTimeout(() => {
|
||||
setIsRefreshingLogs(false)
|
||||
}, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLogsDisplay("")
|
||||
setInfoDisplay("");
|
||||
if (!container) return
|
||||
(async () => {
|
||||
const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])
|
||||
setLogsDisplay(logsHtml)
|
||||
setInfoDisplay(infoHtml)
|
||||
setTimeout(scrollLogsToBottom, 20)
|
||||
})()
|
||||
}, [container])
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogsFullscreenDialog
|
||||
open={logsFullscreenOpen}
|
||||
onOpenChange={setLogsFullscreenOpen}
|
||||
logsDisplay={logsDisplay}
|
||||
containerName={container.name}
|
||||
onRefresh={refreshLogs}
|
||||
isRefreshing={isRefreshingLogs}
|
||||
/>
|
||||
<InfoFullscreenDialog
|
||||
open={infoFullscreenOpen}
|
||||
onOpenChange={setInfoFullscreenOpen}
|
||||
infoDisplay={infoDisplay}
|
||||
containerName={container.name}
|
||||
/>
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetContent className="w-full sm:max-w-220 p-2">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{container.name}</SheetTitle>
|
||||
<SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<Link className="hover:underline" href={getPagePath($router, "system", { id: container.system })}>{$allSystemsById.get()[container.system]?.name ?? ""}</Link>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
{container.status}
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
{container.image}
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
{container.id}
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
{ContainerHealthLabels[container.health as ContainerHealth]}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="px-3 pb-3 -mt-4 flex flex-col gap-3 h-full items-start">
|
||||
<div className="flex items-center w-full">
|
||||
<h3>{t`Logs`}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={refreshLogs}
|
||||
className="h-8 w-8 p-0 ms-auto"
|
||||
disabled={isRefreshingLogs}
|
||||
>
|
||||
<RefreshCwIcon
|
||||
className={`size-4 transition-transform duration-300 ${isRefreshingLogs ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setLogsFullscreenOpen(true)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<MaximizeIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div ref={logsContainerRef} className={cn("max-h-[calc(50dvh-10rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm", !logsDisplay && ["animate-pulse", "h-full"])}>
|
||||
<div dangerouslySetInnerHTML={{ __html: logsDisplay }} />
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<h3>{t`Detail`}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setInfoFullscreenOpen(true)}
|
||||
className="h-8 w-8 p-0 ms-auto"
|
||||
>
|
||||
<MaximizeIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={cn("grow h-[calc(50dvh-4rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm", !infoDisplay && "animate-pulse")}>
|
||||
<div dangerouslySetInnerHTML={{ __html: infoDisplay }} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
function ContainersTableHead({ table }: { table: TableType<ContainerRecord> }) {
|
||||
return (
|
||||
<TableHeader className="sticky top-0 z-50 w-full border-b-2">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr 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>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</TableHeader>
|
||||
)
|
||||
}
|
||||
|
||||
const ContainerTableRow = memo(
|
||||
function ContainerTableRow({
|
||||
row,
|
||||
virtualRow,
|
||||
openSheet,
|
||||
}: {
|
||||
row: Row<ContainerRecord>
|
||||
virtualRow: VirtualItem
|
||||
openSheet: (container: ContainerRecord) => void
|
||||
}) {
|
||||
return (
|
||||
<TableRow
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="cursor-pointer transition-opacity"
|
||||
onClick={() => openSheet(row.original)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="py-0"
|
||||
style={{
|
||||
height: virtualRow.size,
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
function LogsFullscreenDialog({ open, onOpenChange, logsDisplay, containerName, onRefresh, isRefreshing }: { open: boolean, onOpenChange: (open: boolean) => void, logsDisplay: string, containerName: string, onRefresh: () => void | Promise<void>, isRefreshing: boolean }) {
|
||||
const outerContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && logsDisplay) {
|
||||
// Scroll the outer container to bottom
|
||||
const scrollToBottom = () => {
|
||||
if (outerContainerRef.current) {
|
||||
outerContainerRef.current.scrollTop = outerContainerRef.current.scrollHeight
|
||||
}
|
||||
}
|
||||
setTimeout(scrollToBottom, 50)
|
||||
}
|
||||
}, [open, logsDisplay])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white">
|
||||
<DialogTitle className="sr-only">{containerName} logs</DialogTitle>
|
||||
<div ref={outerContainerRef} className="h-full overflow-auto">
|
||||
<div className="h-full w-full px-3 leading-relaxed rounded-md bg-gh-dark text-sm">
|
||||
<div className="py-3" dangerouslySetInnerHTML={{ __html: logsDisplay }} />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
void onRefresh()
|
||||
}}
|
||||
className="absolute top-3 right-11 opacity-60 hover:opacity-100 p-1"
|
||||
disabled={isRefreshing}
|
||||
title={t`Refresh`}
|
||||
aria-label={t`Refresh`}
|
||||
>
|
||||
<RefreshCwIcon
|
||||
className={`size-4 transition-transform duration-300 ${isRefreshing ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function InfoFullscreenDialog({ open, onOpenChange, infoDisplay, containerName }: { open: boolean, onOpenChange: (open: boolean) => void, infoDisplay: string, containerName: string }) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white">
|
||||
<DialogTitle className="sr-only">{containerName} info</DialogTitle>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="h-full w-full overflow-auto p-3 rounded-md bg-gh-dark text-sm leading-relaxed">
|
||||
<div dangerouslySetInnerHTML={{ __html: infoDisplay }} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
26
internal/site/src/components/footer-repo-link.tsx
Normal file
26
internal/site/src/components/footer-repo-link.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { Separator } from "./ui/separator"
|
||||
|
||||
export function FooterRepoLink() {
|
||||
return (
|
||||
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
|
||||
<a
|
||||
href="https://github.com/henrygd/beszel"
|
||||
target="_blank"
|
||||
className="flex items-center gap-0.5 text-muted-foreground hover:text-foreground duration-75"
|
||||
rel="noopener"
|
||||
>
|
||||
<GithubIcon className="h-3 w-3" /> GitHub
|
||||
</a>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
<a
|
||||
href="https://github.com/henrygd/beszel/releases"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-foreground duration-75"
|
||||
rel="noopener"
|
||||
>
|
||||
Beszel {globalThis.BESZEL.HUB_VERSION}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function LangToggle() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={"ghost"} size="icon" className="hidden 450:flex">
|
||||
<Button variant={"ghost"} size="icon" className="hidden sm:flex">
|
||||
<LanguagesIcon className="absolute h-[1.2rem] w-[1.2rem] light:opacity-85" />
|
||||
<span className="sr-only">Language</span>
|
||||
</Button>
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
import { useId } from "react"
|
||||
|
||||
const d = "M146.4 73.1h-30.5V59.8h30.5a3.2 3.2 0 0 0 2.3-1 3.2 3.2 0 0 0 1-2.3q0-.8-.3-1.3a1.5 1.5 0 0 0-.7-.6 4.7 4.7 0 0 0-1-.3l-1.3-.1h-13.9q-3.4 0-6.5-1.3-3-1.3-5.2-3.6a16.9 16.9 0 0 1-3.6-5.3 16.3 16.3 0 0 1-1.3-6.5 16.4 16.4 0 0 1 1.3-6.4q1.3-3.1 3.6-5.4 2.2-2.2 5.2-3.5a16.3 16.3 0 0 1 6.5-1.3h27v13.3h-27a3.2 3.2 0 0 0-2.3 1 3.2 3.2 0 0 0-1 2.3 3.3 3.3 0 0 0 1 2.4 3.3 3.3 0 0 0 1.2.8 3.2 3.2 0 0 0 1.1.2h13.9a18.1 18.1 0 0 1 6 1 17.3 17.3 0 0 1 .4.2q3 1.1 5.3 3.2a15.1 15.1 0 0 1 3.6 4.9 14.7 14.7 0 0 1 1.3 5.4 17.2 17.2 0 0 1 0 .9 16 16 0 0 1-1 5.8 15.4 15.4 0 0 1-.3.7 17.3 17.3 0 0 1-3.6 5.2 16.4 16.4 0 0 1-5.3 3.6 16.2 16.2 0 0 1-6.4 1.3Zm64.5-13.3v13.3h-43.6l22-39h-22V21h43.6l-22 39h22ZM35 73.1H0v-70h35q4.4 0 8.2 1.6a21.4 21.4 0 0 1 6.6 4.6q2.9 2.8 4.5 6.6 1.7 3.8 1.7 8.2a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5 1.4 1.6 2.4 3.5a18.3 18.3 0 0 1 1.5 4A17.4 17.4 0 0 1 56 51a15.3 15.3 0 0 1 0 1.1q0 4.3-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.5-3.8 1.7-8.2 1.7Zm76-43L86 60.4l1.5.3a16.7 16.7 0 0 0 1.6 0q2 0 3.8-.4 1.8-.6 3.4-1.6 1.6-1 2.8-2.4a12.8 12.8 0 0 0 2-3.2l9.8 9.8q-1.9 2.6-4.3 4.7a27 27 0 0 1-5.2 3.6 26.1 26.1 0 0 1-6 2.2 26.8 26.8 0 0 1-6.3.8 26.4 26.4 0 0 1-10.4-2 26.2 26.2 0 0 1-8.5-5.8 26.7 26.7 0 0 1-5.5-8.3 30.4 30.4 0 0 1-.2-.4q-2.1-5-2.1-11.1a31.9 31.9 0 0 1 .7-7 27 27 0 0 1 1.4-4.3 27 27 0 0 1 3.8-6.6 24.5 24.5 0 0 1 2-2.2 26 26 0 0 1 8.4-5.6 27 27 0 0 1 10.4-2 26.3 26.3 0 0 1 6.4.8 26.9 26.9 0 0 1 6 2.2q2.7 1.5 5.2 3.6 2.4 2.1 4.3 4.8Zm152.3 0-25 30.2 1.5.3a16.7 16.7 0 0 0 1.6 0q2 0 3.8-.4 1.8-.6 3.4-1.6 1.5-1 2.8-2.4a12.8 12.8 0 0 0 2-3.2l9.8 9.8q-1.9 2.6-4.3 4.7a27 27 0 0 1-5.2 3.6 26.1 26.1 0 0 1-6 2.2 26.8 26.8 0 0 1-6.3.8 26.4 26.4 0 0 1-10.4-2 26.2 26.2 0 0 1-8.5-5.8A26.7 26.7 0 0 1 217 58a30.4 30.4 0 0 1-.2-.4q-2.1-5-2.1-11.1a31.9 31.9 0 0 1 .7-7 27 27 0 0 1 1.4-4.3 27 27 0 0 1 3.8-6.6 24.5 24.5 0 0 1 2-2.2 26 26 0 0 1 8.4-5.6 27 27 0 0 1 10.4-2 26.3 26.3 0 0 1 6.4.8 26.9 26.9 0 0 1 6 2.2q2.7 1.5 5.2 3.6 2.4 2.1 4.3 4.8ZM283.4 0v73.1H270V0h13.4ZM14 17v14.1h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 30 40 29a6.9 6.9 0 0 0 1.5-2.3q.5-1.3.5-2.7a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.5q-.6-1.2-1.5-2.2a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.5 7.9 7.9 0 0 0-.2 0H14Zm0 28.1v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.2Q39 58 40 57.1a7 7 0 0 0 1.5-2.3 6.9 6.9 0 0 0 .5-2.5 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 48 40 47a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm63.3 8.3 15.5-20.6a8 8 0 0 0-1.4-.4 7 7 0 0 0-.4 0 17.2 17.2 0 0 0-1.6-.1 19.2 19.2 0 0 0-.3 0 13.3 13.3 0 0 0-5.1 1q-2.5 1-4.2 2.8a13.1 13.1 0 0 0-2.5 3.6 15.5 15.5 0 0 0-.3.9 14.7 14.7 0 0 0-1 3.5 18.7 18.7 0 0 0 0 2.4 17.6 17.6 0 0 0 0 .7v.8a29.4 29.4 0 0 0 0 .1 19.2 19.2 0 0 0 .2 2 20.2 20.2 0 0 0 .4 1.6 18.6 18.6 0 0 0 0 .2 7.5 7.5 0 0 0 .4.9 6 6 0 0 0 .3.6Zm152.3 0L245 32.8a8 8 0 0 0-1.4-.4 7 7 0 0 0-.4 0 17.2 17.2 0 0 0-1.6-.1 19.2 19.2 0 0 0-.3 0 13.3 13.3 0 0 0-5.1 1q-2.5 1-4.2 2.8a13.1 13.1 0 0 0-2.5 3.6 15.5 15.5 0 0 0-.4.9 14.7 14.7 0 0 0-.8 3.5 18.7 18.7 0 0 0-.2 2.4 17.6 17.6 0 0 0 0 .7v.8a29.4 29.4 0 0 0 .1.1 19.2 19.2 0 0 0 .2 2 20.2 20.2 0 0 0 .4 1.6 18.6 18.6 0 0 0 0 .2 7.5 7.5 0 0 0 .4.9 6 6 0 0 0 .3.6Z"
|
||||
|
||||
export function Logo({ className }: { className?: string }) {
|
||||
const id = useId()
|
||||
|
||||
return (
|
||||
// Righteous
|
||||
// Righteous font from Google Fonts
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 285 75" className={className}>
|
||||
{/* <defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="20%" x2="100%" y2="120%">
|
||||
<stop offset="0%" style={{ stopColor: "#747bff" }} />
|
||||
<stop offset="100%" style={{ stopColor: "#24eb5c" }} />
|
||||
<defs>
|
||||
<linearGradient id={id} x1="0%" y1="20%" x2="100%" y2="120%">
|
||||
<stop offset="10%" style={{ stopColor: "#747bff" }} />
|
||||
<stop offset="90%" style={{ stopColor: "#24eb5c" }} />
|
||||
</linearGradient>
|
||||
</defs> */}
|
||||
</defs>
|
||||
<path
|
||||
// fill="url(#gradient)"
|
||||
d="M146.4 73.1h-30.5V59.8h30.5a3.2 3.2 0 0 0 2.3-1 3.2 3.2 0 0 0 1-2.3q0-.8-.3-1.3a1.5 1.5 0 0 0-.7-.6 4.7 4.7 0 0 0-1-.3l-1.3-.1h-13.9q-3.4 0-6.5-1.3-3-1.3-5.2-3.6a16.9 16.9 0 0 1-3.6-5.3 16.3 16.3 0 0 1-1.3-6.5 16.4 16.4 0 0 1 1.3-6.4q1.3-3.1 3.6-5.4 2.2-2.2 5.2-3.5a16.3 16.3 0 0 1 6.5-1.3h27v13.3h-27a3.2 3.2 0 0 0-2.3 1 3.2 3.2 0 0 0-1 2.3 3.3 3.3 0 0 0 1 2.4 3.3 3.3 0 0 0 1.2.8 3.2 3.2 0 0 0 1.1.2h13.9a18.1 18.1 0 0 1 6 1 17.3 17.3 0 0 1 .4.2q3 1.1 5.3 3.2a15.1 15.1 0 0 1 3.6 4.9 14.7 14.7 0 0 1 1.3 5.4 17.2 17.2 0 0 1 0 .9 16 16 0 0 1-1 5.8 15.4 15.4 0 0 1-.3.7 17.3 17.3 0 0 1-3.6 5.2 16.4 16.4 0 0 1-5.3 3.6 16.2 16.2 0 0 1-6.4 1.3Zm64.5-13.3v13.3h-43.6l22-39h-22V21h43.6l-22 39h22ZM35 73.1H0v-70h35q4.4 0 8.2 1.6a21.4 21.4 0 0 1 6.6 4.6q2.9 2.8 4.5 6.6 1.7 3.8 1.7 8.2a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5 1.4 1.6 2.4 3.5a18.3 18.3 0 0 1 1.5 4A17.4 17.4 0 0 1 56 51a15.3 15.3 0 0 1 0 1.1q0 4.3-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.5-3.8 1.7-8.2 1.7Zm76-43L86 60.4l1.5.3a16.7 16.7 0 0 0 1.6 0q2 0 3.8-.4 1.8-.6 3.4-1.6 1.6-1 2.8-2.4a12.8 12.8 0 0 0 2-3.2l9.8 9.8q-1.9 2.6-4.3 4.7a27 27 0 0 1-5.2 3.6 26.1 26.1 0 0 1-6 2.2 26.8 26.8 0 0 1-6.3.8 26.4 26.4 0 0 1-10.4-2 26.2 26.2 0 0 1-8.5-5.8 26.7 26.7 0 0 1-5.5-8.3 30.4 30.4 0 0 1-.2-.4q-2.1-5-2.1-11.1a31.9 31.9 0 0 1 .7-7 27 27 0 0 1 1.4-4.3 27 27 0 0 1 3.8-6.6 24.5 24.5 0 0 1 2-2.2 26 26 0 0 1 8.4-5.6 27 27 0 0 1 10.4-2 26.3 26.3 0 0 1 6.4.8 26.9 26.9 0 0 1 6 2.2q2.7 1.5 5.2 3.6 2.4 2.1 4.3 4.8Zm152.3 0-25 30.2 1.5.3a16.7 16.7 0 0 0 1.6 0q2 0 3.8-.4 1.8-.6 3.4-1.6 1.5-1 2.8-2.4a12.8 12.8 0 0 0 2-3.2l9.8 9.8q-1.9 2.6-4.3 4.7a27 27 0 0 1-5.2 3.6 26.1 26.1 0 0 1-6 2.2 26.8 26.8 0 0 1-6.3.8 26.4 26.4 0 0 1-10.4-2 26.2 26.2 0 0 1-8.5-5.8A26.7 26.7 0 0 1 217 58a30.4 30.4 0 0 1-.2-.4q-2.1-5-2.1-11.1a31.9 31.9 0 0 1 .7-7 27 27 0 0 1 1.4-4.3 27 27 0 0 1 3.8-6.6 24.5 24.5 0 0 1 2-2.2 26 26 0 0 1 8.4-5.6 27 27 0 0 1 10.4-2 26.3 26.3 0 0 1 6.4.8 26.9 26.9 0 0 1 6 2.2q2.7 1.5 5.2 3.6 2.4 2.1 4.3 4.8ZM283.4 0v73.1H270V0h13.4ZM14 17v14.1h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 30 40 29a6.9 6.9 0 0 0 1.5-2.3q.5-1.3.5-2.7a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.5q-.6-1.2-1.5-2.2a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.5 7.9 7.9 0 0 0-.2 0H14Zm0 28.1v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.2Q39 58 40 57.1a7 7 0 0 0 1.5-2.3 6.9 6.9 0 0 0 .5-2.5 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 48 40 47a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm63.3 8.3 15.5-20.6a8 8 0 0 0-1.4-.4 7 7 0 0 0-.4 0 17.2 17.2 0 0 0-1.6-.1 19.2 19.2 0 0 0-.3 0 13.3 13.3 0 0 0-5.1 1q-2.5 1-4.2 2.8a13.1 13.1 0 0 0-2.5 3.6 15.5 15.5 0 0 0-.3.9 14.7 14.7 0 0 0-1 3.5 18.7 18.7 0 0 0 0 2.4 17.6 17.6 0 0 0 0 .7v.8a29.4 29.4 0 0 0 0 .1 19.2 19.2 0 0 0 .2 2 20.2 20.2 0 0 0 .4 1.6 18.6 18.6 0 0 0 0 .2 7.5 7.5 0 0 0 .4.9 6 6 0 0 0 .3.6Zm152.3 0L245 32.8a8 8 0 0 0-1.4-.4 7 7 0 0 0-.4 0 17.2 17.2 0 0 0-1.6-.1 19.2 19.2 0 0 0-.3 0 13.3 13.3 0 0 0-5.1 1q-2.5 1-4.2 2.8a13.1 13.1 0 0 0-2.5 3.6 15.5 15.5 0 0 0-.4.9 14.7 14.7 0 0 0-.8 3.5 18.7 18.7 0 0 0-.2 2.4 17.6 17.6 0 0 0 0 .7v.8a29.4 29.4 0 0 0 .1.1 19.2 19.2 0 0 0 .2 2 20.2 20.2 0 0 0 .4 1.6 18.6 18.6 0 0 0 0 .2 7.5 7.5 0 0 0 .4.9 6 6 0 0 0 .3.6Z"
|
||||
className="duration-250 group-hover:opacity-0 group-hover:ease-in ease-out"
|
||||
d={d}
|
||||
/>
|
||||
<path
|
||||
className="opacity-0 duration-250 group-hover:opacity-100 ease-in-out"
|
||||
fill={`url(#${id})`}
|
||||
d={d}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import {
|
||||
ContainerIcon,
|
||||
DatabaseBackupIcon,
|
||||
LogOutIcon,
|
||||
LogsIcon,
|
||||
@@ -39,7 +40,7 @@ export default function Navbar() {
|
||||
<Link
|
||||
href={basePath}
|
||||
aria-label="Home"
|
||||
className="p-2 ps-0 me-3"
|
||||
className="p-2 ps-0 me-3 group"
|
||||
onMouseEnter={runOnce(() => import("@/components/routes/home"))}
|
||||
>
|
||||
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
|
||||
@@ -47,18 +48,25 @@ export default function Navbar() {
|
||||
<SearchButton />
|
||||
|
||||
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
|
||||
<Link
|
||||
href={getPagePath($router, "containers")}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
aria-label="Containers"
|
||||
>
|
||||
<ContainerIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
||||
</Link>
|
||||
<LangToggle />
|
||||
<ModeToggle />
|
||||
<Link
|
||||
href={getPagePath($router, "settings", { name: "general" })}
|
||||
aria-label="Settings"
|
||||
className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
>
|
||||
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button aria-label="User Actions" className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}>
|
||||
<button aria-label="User Actions" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
|
||||
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -112,7 +120,7 @@ export default function Navbar() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AddSystemButton className="ms-2" />
|
||||
<AddSystemButton className="ms-2 hidden 450:flex" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createRouter } from "@nanostores/router"
|
||||
|
||||
const routes = {
|
||||
home: "/",
|
||||
containers: "/containers",
|
||||
system: `/system/:id`,
|
||||
settings: `/settings/:name?`,
|
||||
forgot_password: `/forgot-password`,
|
||||
|
||||
26
internal/site/src/components/routes/containers.tsx
Normal file
26
internal/site/src/components/routes/containers.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { memo, useEffect, useMemo } from "react"
|
||||
import ContainersTable from "@/components/containers-table/containers-table"
|
||||
import { ActiveAlerts } from "@/components/active-alerts"
|
||||
import { FooterRepoLink } from "@/components/footer-repo-link"
|
||||
|
||||
export default memo(() => {
|
||||
const { t } = useLingui()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t`All Containers`} / Beszel`
|
||||
}, [t])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div className="grid gap-4">
|
||||
<ActiveAlerts />
|
||||
<ContainersTable />
|
||||
</div>
|
||||
<FooterRepoLink />
|
||||
</>
|
||||
),
|
||||
[]
|
||||
)
|
||||
})
|
||||
@@ -1,128 +1,28 @@
|
||||
import { Plural, Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { GithubIcon } from "lucide-react"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { memo, Suspense, useEffect, useMemo } from "react"
|
||||
import { $router, Link } from "@/components/router"
|
||||
import SystemsTable from "@/components/systems-table/systems-table"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { $alerts, $allSystemsById } from "@/lib/stores"
|
||||
import type { AlertRecord } from "@/types"
|
||||
import { ActiveAlerts } from "@/components/active-alerts"
|
||||
import { FooterRepoLink } from "@/components/footer-repo-link"
|
||||
|
||||
export default memo(() => {
|
||||
const { t } = useLingui()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t`Dashboard`} / Beszel`
|
||||
document.title = `${t`All Systems`} / Beszel`
|
||||
}, [t])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
<ActiveAlerts />
|
||||
<Suspense>
|
||||
<SystemsTable />
|
||||
</Suspense>
|
||||
|
||||
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
|
||||
<a
|
||||
href="https://github.com/henrygd/beszel"
|
||||
target="_blank"
|
||||
className="flex items-center gap-0.5 text-muted-foreground hover:text-foreground duration-75"
|
||||
rel="noopener"
|
||||
>
|
||||
<GithubIcon className="h-3 w-3" /> GitHub
|
||||
</a>
|
||||
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
<a
|
||||
href="https://github.com/henrygd/beszel/releases"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-foreground duration-75"
|
||||
rel="noopener"
|
||||
>
|
||||
Beszel {globalThis.BESZEL.HUB_VERSION}
|
||||
</a>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ActiveAlerts />
|
||||
<Suspense>
|
||||
<SystemsTable />
|
||||
</Suspense>
|
||||
</div>
|
||||
<FooterRepoLink />
|
||||
</>
|
||||
),
|
||||
[]
|
||||
)
|
||||
})
|
||||
|
||||
const ActiveAlerts = () => {
|
||||
const alerts = useStore($alerts)
|
||||
const systems = useStore($allSystemsById)
|
||||
|
||||
const { activeAlerts, alertsKey } = useMemo(() => {
|
||||
const activeAlerts: AlertRecord[] = []
|
||||
// key to prevent re-rendering if alerts change but active alerts didn't
|
||||
const alertsKey: string[] = []
|
||||
|
||||
for (const systemId of Object.keys(alerts)) {
|
||||
for (const alert of alerts[systemId].values()) {
|
||||
if (alert.triggered && alert.name in alertInfo) {
|
||||
activeAlerts.push(alert)
|
||||
alertsKey.push(`${alert.system}${alert.value}${alert.min}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { activeAlerts, alertsKey }
|
||||
}, [alerts])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: alertsKey is inclusive
|
||||
return useMemo(() => {
|
||||
if (activeAlerts.length === 0) {
|
||||
return null
|
||||
}
|
||||
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-px duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black/5"
|
||||
>
|
||||
<info.icon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{alert.name === "Status" ? (
|
||||
<Trans>Connection is down</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
|
||||
</Trans>
|
||||
)}
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { id: systems[alert.system]?.id })}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
aria-label="View system"
|
||||
></Link>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}, [alertsKey.join("")])
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function ConfigYaml() {
|
||||
</Trans>
|
||||
</p>
|
||||
<Alert className="my-4 border-destructive text-destructive w-auto table md:pe-6">
|
||||
<AlertCircleIcon className="h-4 w-4 stroke-destructive" />
|
||||
<AlertCircleIcon className="size-4.5 stroke-destructive" />
|
||||
<AlertTitle>
|
||||
<Trans>Caution - potential data loss</Trans>
|
||||
</AlertTitle>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Plural, Trans, useLingui } from "@lingui/react/macro"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { timeTicks } from "d3-time"
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react"
|
||||
import { subscribeKeys } from "nanostores"
|
||||
import React, { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
|
||||
import ContainerChart from "@/components/charts/container-chart"
|
||||
import DiskChart from "@/components/charts/disk-chart"
|
||||
@@ -41,9 +41,11 @@ import { useIntersectionObserver } from "@/lib/use-intersection-observer"
|
||||
import {
|
||||
chartTimeData,
|
||||
cn,
|
||||
compareSemVer,
|
||||
debounce,
|
||||
decimalString,
|
||||
formatBytes,
|
||||
secondsToString,
|
||||
getHostDisplayValue,
|
||||
listen,
|
||||
parseSemVer,
|
||||
@@ -71,8 +73,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
||||
import { Separator } from "../ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||
import NetworkSheet from "./system/network-sheet"
|
||||
import CpuCoresSheet from "./system/cpu-sheet"
|
||||
import LineChartDefault from "../charts/line-chart"
|
||||
|
||||
|
||||
|
||||
type ChartTimeData = {
|
||||
time: number
|
||||
data: {
|
||||
@@ -93,8 +98,8 @@ function getTimeData(chartTime: ChartTimes, lastCreated: number) {
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = chartTime === "1m" ? 400 : 20_000
|
||||
const now = new Date(Date.now() + buffer)
|
||||
// const buffer = chartTime === "1m" ? 400 : 20_000
|
||||
const now = new Date(Date.now())
|
||||
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
|
||||
const data = {
|
||||
@@ -168,7 +173,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
const [system, setSystem] = useState({} as SystemRecord)
|
||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
|
||||
const netCardRef = useRef<HTMLDivElement>(null)
|
||||
const temperatureChartRef = useRef<HTMLDivElement>(null)
|
||||
const persistChartTime = useRef(false)
|
||||
const [bottomSpacing, setBottomSpacing] = useState(0)
|
||||
const [chartLoading, setChartLoading] = useState(true)
|
||||
@@ -214,7 +219,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
// subscribe to realtime metrics if chart time is 1m
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||
useEffect(() => {
|
||||
let unsub = () => {}
|
||||
let unsub = () => { }
|
||||
if (!system.id || chartTime !== "1m") {
|
||||
return
|
||||
}
|
||||
@@ -355,21 +360,13 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
value: system.info.k,
|
||||
},
|
||||
}
|
||||
let uptime: React.ReactNode
|
||||
let uptime: string
|
||||
if (system.info.u < 3600) {
|
||||
uptime = (
|
||||
<Plural
|
||||
value={Math.trunc(system.info.u / 60)}
|
||||
one="# minute"
|
||||
few="# minutes"
|
||||
many="# minutes"
|
||||
other="# minutes"
|
||||
/>
|
||||
)
|
||||
} else if (system.info.u < 172800) {
|
||||
uptime = <Plural value={Math.trunc(system.info.u / 3600)} one="# hour" other="# hours" />
|
||||
uptime = secondsToString(system.info.u, "minute")
|
||||
} else if (system.info.u < 360000) {
|
||||
uptime = secondsToString(system.info.u, "hour")
|
||||
} else {
|
||||
uptime = <Plural value={Math.trunc(system.info?.u / 86400)} one="# day" other="# days" />
|
||||
uptime = secondsToString(system.info.u, "day")
|
||||
}
|
||||
return [
|
||||
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
|
||||
@@ -395,19 +392,20 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
}[]
|
||||
}, [system, t])
|
||||
|
||||
/** Space for tooltip if more than 12 containers */
|
||||
/** Space for tooltip if more than 10 sensors and no containers table */
|
||||
useEffect(() => {
|
||||
if (!netCardRef.current || !containerData.length) {
|
||||
const sensors = Object.keys(systemStats.at(-1)?.stats.t ?? {})
|
||||
if (!temperatureChartRef.current || sensors.length < 10 || containerData.length > 0) {
|
||||
setBottomSpacing(0)
|
||||
return
|
||||
}
|
||||
const tooltipHeight = (Object.keys(containerData[0]).length - 11) * 17.8 - 40
|
||||
const tooltipHeight = (sensors.length - 10) * 17.8 - 40
|
||||
const wrapperEl = chartWrapRef.current as HTMLDivElement
|
||||
const wrapperRect = wrapperEl.getBoundingClientRect()
|
||||
const chartRect = netCardRef.current.getBoundingClientRect()
|
||||
const chartRect = temperatureChartRef.current.getBoundingClientRect()
|
||||
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
|
||||
setBottomSpacing(tooltipHeight - distanceToBottom)
|
||||
}, [containerData])
|
||||
}, [])
|
||||
|
||||
// keyboard navigation between systems
|
||||
useEffect(() => {
|
||||
@@ -569,6 +567,18 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* <Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="w-full h-11">
|
||||
<TabsTrigger value="overview" className="w-full h-9">Overview</TabsTrigger>
|
||||
<TabsTrigger value="containers" className="w-full h-9">Containers</TabsTrigger>
|
||||
<TabsTrigger value="smart" className="w-full h-9">S.M.A.R.T.</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="smart">
|
||||
</TabsContent>
|
||||
</Tabs> */}
|
||||
|
||||
|
||||
{/* main charts */}
|
||||
<div className="grid xl:grid-cols-2 gap-4">
|
||||
<ChartCard
|
||||
@@ -576,7 +586,12 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
grid={grid}
|
||||
title={t`CPU Usage`}
|
||||
description={t`Average system-wide CPU utilization`}
|
||||
cornerEl={maxValSelect}
|
||||
cornerEl={
|
||||
<div className="flex gap-2">
|
||||
{maxValSelect}
|
||||
<CpuCoresSheet chartData={chartData} dataEmpty={dataEmpty} grid={grid} maxValues={maxValues} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
@@ -741,26 +756,20 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && containerData.length > 0 && (
|
||||
<div
|
||||
ref={netCardRef}
|
||||
className={cn({
|
||||
"col-span-full": !grid,
|
||||
})}
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={dockerOrPodman(t`Docker Network I/O`, system)}
|
||||
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
title={dockerOrPodman(t`Docker Network I/O`, system)}
|
||||
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
|
||||
cornerEl={containerFilterBar}
|
||||
>
|
||||
<ContainerChart
|
||||
chartData={chartData}
|
||||
chartType={ChartType.Network}
|
||||
dataKey="n"
|
||||
chartConfig={containerChartConfigs.network}
|
||||
/>
|
||||
</ChartCard>
|
||||
</div>
|
||||
<ContainerChart
|
||||
chartData={chartData}
|
||||
chartType={ChartType.Network}
|
||||
dataKey="n"
|
||||
chartConfig={containerChartConfigs.network}
|
||||
/>
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{/* Swap chart */}
|
||||
@@ -790,16 +799,21 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
|
||||
{/* Temperature chart */}
|
||||
{systemStats.at(-1)?.stats.t && (
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Temperature`}
|
||||
description={t`Temperatures of system sensors`}
|
||||
cornerEl={<FilterBar store={$temperatureFilter} />}
|
||||
legend={Object.keys(systemStats.at(-1)?.stats.t ?? {}).length < 12}
|
||||
<div
|
||||
ref={temperatureChartRef}
|
||||
className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}
|
||||
>
|
||||
<TemperatureChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Temperature`}
|
||||
description={t`Temperatures of system sensors`}
|
||||
cornerEl={<FilterBar store={$temperatureFilter} />}
|
||||
legend={Object.keys(systemStats.at(-1)?.stats.t ?? {}).length < 12}
|
||||
>
|
||||
<TemperatureChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Battery chart */}
|
||||
@@ -932,7 +946,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
>
|
||||
<DiskChart
|
||||
chartData={chartData}
|
||||
dataKey={`stats.efs.${extraFsName}.du`}
|
||||
dataKey={({ stats }: SystemStatsRecord) => stats?.efs?.[extraFsName]?.du}
|
||||
diskSize={systemStats.at(-1)?.stats.efs?.[extraFsName].d ?? NaN}
|
||||
/>
|
||||
</ChartCard>
|
||||
@@ -950,9 +964,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
label: t`Write`,
|
||||
dataKey: ({ stats }) => {
|
||||
if (showMax) {
|
||||
return stats?.efs?.[extraFsName]?.wb ?? (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
|
||||
return stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
|
||||
}
|
||||
return stats?.efs?.[extraFsName]?.wb ?? (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
|
||||
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
|
||||
},
|
||||
color: 3,
|
||||
opacity: 0.3,
|
||||
@@ -987,9 +1001,17 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && (
|
||||
<LazySmartTable systemId={system.id} />
|
||||
)}
|
||||
|
||||
{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer("0.14.0")) >= 0 && (
|
||||
<LazyContainersTable systemId={id} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* add space for tooltip if more than 12 containers */}
|
||||
{/* add space for tooltip if lots of sensors */}
|
||||
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
|
||||
</>
|
||||
)
|
||||
@@ -1103,7 +1125,7 @@ export function ChartCard({
|
||||
<CardDescription>{description}</CardDescription>
|
||||
{cornerEl && <div className="py-1 grid sm:justify-end sm:absolute sm:top-3.5 sm:end-3.5">{cornerEl}</div>}
|
||||
</CardHeader>
|
||||
<div className={cn("ps-0 w-[calc(100%-1.5em)] relative group", legend ? "h-54 md:h-56" : "h-48 md:h-52")}>
|
||||
<div className={cn("ps-0 w-[calc(100%-1.3em)] relative group", legend ? "h-54 md:h-56" : "h-48 md:h-52")}>
|
||||
{
|
||||
<Spinner
|
||||
msg={empty ? t`Waiting for enough records to display` : undefined}
|
||||
@@ -1116,3 +1138,25 @@ export function ChartCard({
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const ContainersTable = lazy(() => import("../containers-table/containers-table"))
|
||||
|
||||
function LazyContainersTable({ systemId }: { systemId: string }) {
|
||||
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
|
||||
return (
|
||||
<div ref={ref} className={cn(isIntersecting && "contents")}>
|
||||
{isIntersecting && <ContainersTable systemId={systemId} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SmartTable = lazy(() => import("./system/smart-table"))
|
||||
|
||||
function LazySmartTable({ systemId }: { systemId: string }) {
|
||||
const { isIntersecting, ref } = useIntersectionObserver({ rootMargin: "90px" })
|
||||
return (
|
||||
<div ref={ref} className={cn(isIntersecting && "contents")}>
|
||||
{isIntersecting && <SmartTable systemId={systemId} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
195
internal/site/src/components/routes/system/cpu-sheet.tsx
Normal file
195
internal/site/src/components/routes/system/cpu-sheet.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { MoreHorizontalIcon } from "lucide-react"
|
||||
import { memo, useRef, useState } from "react"
|
||||
import AreaChartDefault, { DataPoint } from "@/components/charts/area-chart"
|
||||
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { DialogTitle } from "@/components/ui/dialog"
|
||||
import { compareSemVer, decimalString, parseSemVer, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { ChartCard } from "../system"
|
||||
|
||||
const minAgentVersion = parseSemVer("0.15.3")
|
||||
|
||||
export default memo(function CpuCoresSheet({
|
||||
chartData,
|
||||
dataEmpty,
|
||||
grid,
|
||||
maxValues,
|
||||
}: {
|
||||
chartData: ChartData
|
||||
dataEmpty: boolean
|
||||
grid: boolean
|
||||
maxValues: boolean
|
||||
}) {
|
||||
const [cpuCoresOpen, setCpuCoresOpen] = useState(false)
|
||||
const hasOpened = useRef(false)
|
||||
|
||||
const supportsBreakdown = compareSemVer(chartData.agentVersion, minAgentVersion) >= 0
|
||||
|
||||
if (!supportsBreakdown) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (cpuCoresOpen && !hasOpened.current) {
|
||||
hasOpened.current = true
|
||||
}
|
||||
|
||||
// Latest stats snapshot
|
||||
const latest = chartData.systemStats.at(-1)?.stats
|
||||
const cpus = latest?.cpus ?? []
|
||||
const numCores = cpus.length
|
||||
const hasBreakdown = (latest?.cpub?.length ?? 0) > 0
|
||||
|
||||
const breakdownDataPoints = [
|
||||
{
|
||||
label: "System",
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[1],
|
||||
color: 3,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[0],
|
||||
color: 1,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
},
|
||||
{
|
||||
label: "IOWait",
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[2],
|
||||
color: 4,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
},
|
||||
{
|
||||
label: "Steal",
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[3],
|
||||
color: 5,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
},
|
||||
{
|
||||
label: "Idle",
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[4],
|
||||
color: 2,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
},
|
||||
{
|
||||
label: t`Other`,
|
||||
dataKey: ({ stats }: SystemStatsRecord) => {
|
||||
const total = stats?.cpub?.reduce((acc, curr) => acc + curr, 0) ?? 0
|
||||
return total > 0 ? 100 - total : null
|
||||
},
|
||||
color: `hsl(80, 65%, 52%)`,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
},
|
||||
] as DataPoint[]
|
||||
|
||||
|
||||
return (
|
||||
<Sheet open={cpuCoresOpen} onOpenChange={setCpuCoresOpen}>
|
||||
<DialogTitle className="sr-only">{t`CPU Usage`}</DialogTitle>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
title={t`View more`}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3"
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
{hasOpened.current && (
|
||||
<SheetContent aria-describedby={undefined} className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
|
||||
<ChartTimeSelect className="w-[calc(100%-2em)] bg-card" agentVersion={chartData.agentVersion} />
|
||||
{hasBreakdown && (
|
||||
<ChartCard
|
||||
key="cpu-breakdown"
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`CPU Time Breakdown`}
|
||||
description={t`Percentage of time spent in each state`}
|
||||
legend={true}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={maxValues}
|
||||
legend={true}
|
||||
dataPoints={breakdownDataPoints}
|
||||
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
|
||||
contentFormatter={({ value }) => `${decimalString(value)}%`}
|
||||
reverseStackOrder={true}
|
||||
itemSorter={() => 1}
|
||||
domain={[0, 100]}
|
||||
/>
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{numCores > 0 && (
|
||||
<ChartCard
|
||||
key="cpu-cores-all"
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`CPU Cores`}
|
||||
legend={numCores < 10}
|
||||
description={t`Per-core average utilization`}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
hideYAxis={true}
|
||||
chartData={chartData}
|
||||
maxToggled={maxValues}
|
||||
legend={numCores < 10}
|
||||
dataPoints={Array.from({ length: numCores }).map((_, i) => ({
|
||||
label: `CPU ${i}`,
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpus?.[i] ?? 1 / (stats?.cpus?.length ?? 1),
|
||||
color: `hsl(${226 + (((i * 360) / Math.max(1, numCores)) % 360)}, var(--chart-saturation), var(--chart-lightness))`,
|
||||
opacity: 0.35,
|
||||
stackId: "a"
|
||||
}))}
|
||||
tickFormatter={(val) => `${val}%`}
|
||||
contentFormatter={({ value }) => `${value}%`}
|
||||
reverseStackOrder={true}
|
||||
itemSorter={() => 1}
|
||||
/>
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{Array.from({ length: numCores }).map((_, i) => (
|
||||
<ChartCard
|
||||
key={`cpu-core-${i}`}
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`CPU ${i}`}
|
||||
description={t`Per-core average utilization`}
|
||||
legend={false}
|
||||
className="min-h-auto"
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={maxValues}
|
||||
legend={false}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Usage`,
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.cpus?.[i],
|
||||
color: `hsl(${226 + (((i * 360) / Math.max(1, numCores)) % 360)}, 65%, 52%)`,
|
||||
opacity: 0.35,
|
||||
},
|
||||
]}
|
||||
tickFormatter={(val) => `${val}%`}
|
||||
contentFormatter={({ value }) => `${value}%`}
|
||||
/>
|
||||
</ChartCard>
|
||||
))}
|
||||
</SheetContent>
|
||||
)}
|
||||
</Sheet>
|
||||
)
|
||||
})
|
||||
@@ -53,7 +53,7 @@ export default memo(function NetworkSheet({
|
||||
</SheetTrigger>
|
||||
{hasOpened.current && (
|
||||
<SheetContent aria-describedby={undefined} className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
|
||||
<ChartTimeSelect className="w-[calc(100%-2em)]" agentVersion={chartData.agentVersion} />
|
||||
<ChartTimeSelect className="w-[calc(100%-2em)] bg-card" agentVersion={chartData.agentVersion} />
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
|
||||
484
internal/site/src/components/routes/system/smart-table.tsx
Normal file
484
internal/site/src/components/routes/system/smart-table.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
import * as React from "react"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
Column,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { Activity, Box, Clock, HardDrive, HashIcon, CpuIcon, BinaryIcon, RotateCwIcon, LoaderCircleIcon, CheckCircle2Icon, XCircleIcon, ArrowLeftRightIcon } from "lucide-react"
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { pb } from "@/lib/api"
|
||||
import { SmartData, SmartAttribute } from "@/types"
|
||||
import { formatBytes, toFixedFloat, formatTemperature, cn, secondsToString } from "@/lib/utils"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ThermometerIcon } from "@/components/ui/icons"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
// Column definition for S.M.A.R.T. attributes table
|
||||
export const smartColumns: ColumnDef<SmartAttribute>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.n,
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.rs || row.rv?.toString(),
|
||||
header: "Value",
|
||||
},
|
||||
{
|
||||
accessorKey: "v",
|
||||
header: "Normalized",
|
||||
},
|
||||
{
|
||||
accessorKey: "w",
|
||||
header: "Worst",
|
||||
},
|
||||
{
|
||||
accessorKey: "t",
|
||||
header: "Threshold",
|
||||
},
|
||||
{
|
||||
// accessorFn: (row) => row.wf,
|
||||
accessorKey: "wf",
|
||||
header: "Failing",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
export type DiskInfo = {
|
||||
device: string
|
||||
model: string
|
||||
serialNumber: string
|
||||
firmwareVersion: string
|
||||
capacity: string
|
||||
status: string
|
||||
temperature: number
|
||||
deviceType: string
|
||||
powerOnHours?: number
|
||||
powerCycles?: number
|
||||
}
|
||||
|
||||
// Function to format capacity display
|
||||
function formatCapacity(bytes: number): string {
|
||||
const { value, unit } = formatBytes(bytes)
|
||||
return `${toFixedFloat(value, value >= 10 ? 1 : 2)} ${unit}`
|
||||
}
|
||||
|
||||
// Function to convert SmartData to DiskInfo
|
||||
function convertSmartDataToDiskInfo(smartDataRecord: Record<string, SmartData>): DiskInfo[] {
|
||||
const unknown = "Unknown"
|
||||
return Object.entries(smartDataRecord).map(([key, smartData]) => ({
|
||||
device: smartData.dn || key,
|
||||
model: smartData.mn || unknown,
|
||||
serialNumber: smartData.sn || unknown,
|
||||
firmwareVersion: smartData.fv || unknown,
|
||||
capacity: smartData.c ? formatCapacity(smartData.c) : unknown,
|
||||
status: smartData.s || unknown,
|
||||
temperature: smartData.t || 0,
|
||||
deviceType: smartData.dt || unknown,
|
||||
// These fields need to be extracted from SmartAttribute if available
|
||||
powerOnHours: smartData.a?.find(attr => {
|
||||
const name = attr.n.toLowerCase();
|
||||
return name.includes("poweronhours") || name.includes("power_on_hours");
|
||||
})?.rv,
|
||||
powerCycles: smartData.a?.find(attr => {
|
||||
const name = attr.n.toLowerCase();
|
||||
return (name.includes("power") && name.includes("cycle")) || name.includes("startstopcycles");
|
||||
})?.rv,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
export const columns: ColumnDef<DiskInfo>[] = [
|
||||
{
|
||||
accessorKey: "device",
|
||||
sortingFn: (a, b) => a.original.device.localeCompare(b.original.device),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium ms-1.5">{row.getValue("device")}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "model",
|
||||
sortingFn: (a, b) => a.original.model.localeCompare(b.original.model),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Model`} Icon={Box} />,
|
||||
cell: ({ row }) => (
|
||||
<div className="max-w-50 truncate ms-1.5" title={row.getValue("model")}>
|
||||
{row.getValue("model")}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "capacity",
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Capacity`} Icon={BinaryIcon} />,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="ms-1.5">{getValue() as string}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "temperature",
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Temp`} Icon={ThermometerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const { value, unit } = formatTemperature(getValue() as number)
|
||||
return <span className="ms-1.5">{`${value} ${unit}`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={Activity} />,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue() as string
|
||||
return (
|
||||
<div className="ms-1.5">
|
||||
<Badge
|
||||
variant={status === "PASSED" ? "success" : status === "FAILED" ? "danger" : "warning"}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "deviceType",
|
||||
sortingFn: (a, b) => a.original.deviceType.localeCompare(b.original.deviceType),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Type`} Icon={ArrowLeftRightIcon} />,
|
||||
cell: ({ getValue }) => (
|
||||
<div className="ms-1.5">
|
||||
<Badge variant="outline" className="uppercase">
|
||||
{getValue() as string}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "powerOnHours",
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t({ message: "Power On", comment: "Power On Time" })} Icon={Clock} />,
|
||||
cell: ({ getValue }) => {
|
||||
const hours = (getValue() ?? 0) as number
|
||||
if (!hours && hours !== 0) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground ms-1.5">
|
||||
N/A
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const seconds = hours * 3600
|
||||
return (
|
||||
<div className="text-sm ms-1.5">
|
||||
<div>{secondsToString(seconds, "hour")}</div>
|
||||
<div className="text-muted-foreground text-xs">{secondsToString(seconds, "day")}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "powerCycles",
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t({ message: "Cycles", comment: "Power Cycles" })} Icon={RotateCwIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const cycles = getValue() as number | undefined
|
||||
if (!cycles && cycles !== 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground ms-1.5">
|
||||
N/A
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <span className="ms-1.5">{cycles}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "serialNumber",
|
||||
sortingFn: (a, b) => a.original.serialNumber.localeCompare(b.original.serialNumber),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Serial Number`} Icon={HashIcon} />,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="ms-1.5">{getValue() as string}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "firmwareVersion",
|
||||
sortingFn: (a, b) => a.original.firmwareVersion.localeCompare(b.original.firmwareVersion),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Firmware`} Icon={CpuIcon} />,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="ms-1.5">{getValue() as string}</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function HeaderButton({ column, name, Icon }: { column: Column<DiskInfo>; name: string; Icon: React.ElementType }) {
|
||||
const isSorted = column.getIsSorted()
|
||||
return (
|
||||
<Button
|
||||
className={cn("h-9 px-3 flex items-center gap-2 duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{Icon && <Icon className="size-4" />}
|
||||
{name}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DisksTable({ systemId }: { systemId: string }) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([{ id: "device", desc: false }])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
const [smartData, setSmartData] = React.useState<Record<string, SmartData> | undefined>(undefined)
|
||||
const [activeDisk, setActiveDisk] = React.useState<DiskInfo | null>(null)
|
||||
const [sheetOpen, setSheetOpen] = React.useState(false)
|
||||
|
||||
const openSheet = (disk: DiskInfo) => {
|
||||
setActiveDisk(disk)
|
||||
setSheetOpen(true)
|
||||
}
|
||||
|
||||
// Fetch smart data when component mounts or systemId changes
|
||||
React.useEffect(() => {
|
||||
if (systemId) {
|
||||
pb.send<Record<string, SmartData>>("/api/beszel/smart", { query: { system: systemId } })
|
||||
.then((data) => {
|
||||
setSmartData(data)
|
||||
})
|
||||
.catch(() => setSmartData({}))
|
||||
}
|
||||
}, [systemId])
|
||||
|
||||
// Convert SmartData to DiskInfo, if no data use empty array
|
||||
const diskData = React.useMemo(() => {
|
||||
return smartData ? convertSmartDataToDiskInfo(smartData) : []
|
||||
}, [smartData])
|
||||
|
||||
|
||||
const table = useReactTable({
|
||||
data: diskData,
|
||||
columns: columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
rowSelection,
|
||||
},
|
||||
})
|
||||
|
||||
if (!diskData.length && !columnFilters.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className="p-6 @container w-full">
|
||||
<CardHeader className="p-0 mb-4">
|
||||
<div className="grid md:flex gap-5 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2">
|
||||
S.M.A.R.T.
|
||||
</CardTitle>
|
||||
<CardDescription className="flex">
|
||||
<Trans>Click on a device to view more information.</Trans>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t`Filter...`}
|
||||
value={(table.getColumn("device")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("device")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="ms-auto px-4 w-full max-w-full md:w-64"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className="rounded-md border text-nowrap">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="px-2">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => openSheet(row.original)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="md:ps-5">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{smartData ? t`No results.` : <LoaderCircleIcon className="animate-spin size-10 opacity-60 mx-auto" />}
|
||||
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
<DiskSheet disk={activeDisk} smartData={smartData?.[activeDisk?.serialNumber ?? ""]} open={sheetOpen} onOpenChange={setSheetOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DiskSheet({ disk, smartData, open, onOpenChange }: { disk: DiskInfo | null; smartData?: SmartData; open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
if (!disk) return null
|
||||
|
||||
const smartAttributes = smartData?.a || []
|
||||
|
||||
// Find all attributes where when failed is not empty
|
||||
const failedAttributes = smartAttributes.filter(attr => attr.wf && attr.wf.trim() !== '')
|
||||
|
||||
// Filter columns to only show those that have values in at least one row
|
||||
const visibleColumns = React.useMemo(() => {
|
||||
return smartColumns.filter(column => {
|
||||
const accessorKey = (column as any).accessorKey as keyof SmartAttribute
|
||||
if (!accessorKey) {
|
||||
return true
|
||||
}
|
||||
// Check if any row has a non-empty value for this column
|
||||
return smartAttributes.some(attr => {
|
||||
return attr[accessorKey] !== undefined
|
||||
})
|
||||
})
|
||||
}, [smartAttributes])
|
||||
|
||||
const table = useReactTable({
|
||||
data: smartAttributes,
|
||||
columns: visibleColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-full sm:max-w-220 gap-0">
|
||||
<SheetHeader className="mb-0 border-b">
|
||||
<SheetTitle><Trans>S.M.A.R.T. Details</Trans> - {disk.device}</SheetTitle>
|
||||
<SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
{disk.model} <Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||
{disk.serialNumber}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-auto p-4 flex flex-col gap-4">
|
||||
<Alert className="pb-3">
|
||||
{smartData?.s === "PASSED" ? (
|
||||
<CheckCircle2Icon className="size-4" />
|
||||
) : (
|
||||
<XCircleIcon className="size-4" />
|
||||
)}
|
||||
<AlertTitle><Trans>S.M.A.R.T. Self-Test</Trans>: {smartData?.s}</AlertTitle>
|
||||
{failedAttributes.length > 0 && (
|
||||
<AlertDescription>
|
||||
<Trans>Failed Attributes:</Trans> {failedAttributes.map(attr => attr.n).join(", ")}
|
||||
</AlertDescription>
|
||||
)}
|
||||
</Alert>
|
||||
{smartAttributes.length > 0 ? (
|
||||
<div className="rounded-md border overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
// Check if the attribute is failed
|
||||
const isFailedAttribute = row.original.wf && row.original.wf.trim() !== '';
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className={isFailedAttribute ? "text-red-600 dark:text-red-400" : ""}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Trans>No S.M.A.R.T. attributes available for this device.</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,9 @@ const badgeVariants = cva(
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
success: "border-transparent bg-green-200 text-green-800",
|
||||
danger: "border-transparent bg-red-200 text-red-800",
|
||||
warning: "border-transparent bg-yellow-200 text-yellow-800",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -20,7 +23,7 @@ const badgeVariants = cva(
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> { }
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
|
||||
@@ -91,16 +91,16 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
unit?: string
|
||||
filter?: string
|
||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||
truncate?: boolean
|
||||
}
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
unit?: string
|
||||
filter?: string
|
||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||
truncate?: boolean
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
@@ -129,7 +129,11 @@ const ChartTooltipContent = React.forwardRef<
|
||||
|
||||
React.useMemo(() => {
|
||||
if (filter) {
|
||||
payload = payload?.filter((item) => (item.name as string)?.toLowerCase().includes(filter.toLowerCase()))
|
||||
const filterTerms = filter.toLowerCase().split(" ").filter(term => term.length > 0)
|
||||
payload = payload?.filter((item) => {
|
||||
const itemName = (item.name as string)?.toLowerCase()
|
||||
return filterTerms.some(term => itemName?.includes(term))
|
||||
})
|
||||
}
|
||||
if (itemSorter) {
|
||||
// @ts-expect-error
|
||||
@@ -250,17 +254,20 @@ const ChartLegend = RechartsPrimitive.Legend
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(({ className, payload, verticalAlign = "bottom" }, ref) => {
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
reverse?: boolean
|
||||
}
|
||||
>(({ className, payload, verticalAlign = "bottom", reverse = false }, ref) => {
|
||||
// const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const reversedPayload = reverse ? [...payload].reverse() : payload
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -270,7 +277,7 @@ const ChartLegendContent = React.forwardRef<
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
{reversedPayload.map((item) => {
|
||||
// const key = `${nameKey || item.dataKey || 'value'}`
|
||||
// const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
--chart-4: hsl(280 65% 60%);
|
||||
--chart-5: hsl(340 75% 55%);
|
||||
--table-header: hsl(225, 6%, 97%);
|
||||
--chart-saturation: 65%;
|
||||
--chart-lightness: 50%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -51,11 +53,13 @@
|
||||
--accent: hsl(220 5% 15.5%);
|
||||
--accent-foreground: hsl(220 2% 98%);
|
||||
--destructive: hsl(0 62% 46%);
|
||||
--border: hsl(220 3% 16%);
|
||||
--border: hsl(220 3% 17%);
|
||||
--input: hsl(220 4% 22%);
|
||||
--ring: hsl(220 4% 80%);
|
||||
--table-header: hsl(220, 6%, 13%);
|
||||
--radius: 0.8rem;
|
||||
--chart-saturation: 60%;
|
||||
--chart-lightness: 55%;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -82,6 +86,8 @@
|
||||
--color-green-900: hsl(140 54% 12%);
|
||||
--color-green-950: hsl(140 57% 6%);
|
||||
|
||||
--color-gh-dark: #22272e;
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
@@ -110,12 +116,14 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
/* Fonts */
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: Inter, InterVariable, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: InterVariable;
|
||||
font-style: normal;
|
||||
@@ -130,16 +138,18 @@
|
||||
@apply border-border outline-ring/50;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@utility container {
|
||||
@apply max-w-360 mx-auto px-4;
|
||||
@apply max-w-370 mx-auto px-4;
|
||||
}
|
||||
|
||||
@utility link {
|
||||
@@ -149,16 +159,17 @@
|
||||
@utility ns-dialog {
|
||||
/* New system dialog width */
|
||||
min-width: 30.3rem;
|
||||
|
||||
:where(:lang(zh), :lang(zh-CN), :lang(ko)) & {
|
||||
min-width: 27.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recharts-tooltip-wrapper {
|
||||
z-index: 1;
|
||||
z-index: 51;
|
||||
@apply tabular-nums;
|
||||
}
|
||||
|
||||
.recharts-yAxis {
|
||||
@apply tabular-nums;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import PocketBase from "pocketbase"
|
||||
import { basePath } from "@/components/router"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import type { ChartTimes, UserSettings } from "@/types"
|
||||
import { $alerts, $allSystemsByName, $userSettings } from "./stores"
|
||||
import { $alerts, $allSystemsById, $allSystemsByName, $userSettings } from "./stores"
|
||||
import { chartTimeData } from "./utils"
|
||||
|
||||
/** PocketBase JS Client */
|
||||
@@ -28,6 +28,7 @@ export const verifyAuth = () => {
|
||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||
export function logOut() {
|
||||
$allSystemsByName.set({})
|
||||
$allSystemsById.set({})
|
||||
$alerts.set({})
|
||||
$userSettings.set({} as UserSettings)
|
||||
sessionStorage.setItem("lo", "t") // prevent auto login on logout
|
||||
|
||||
@@ -54,6 +54,16 @@ export enum HourFormat {
|
||||
"24h" = "24h",
|
||||
}
|
||||
|
||||
/** Container health status */
|
||||
export enum ContainerHealth {
|
||||
None,
|
||||
Starting,
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
|
||||
export const ContainerHealthLabels = ["None", "Starting", "Healthy", "Unhealthy"] as const
|
||||
|
||||
/** Connection type */
|
||||
export enum ConnectionType {
|
||||
SSH = 1,
|
||||
|
||||
@@ -7,13 +7,15 @@ import { messages as enMessages } from "@/locales/en/en"
|
||||
import { BatteryState } from "./enums"
|
||||
import { $direction } from "./stores"
|
||||
|
||||
const rtlLanguages = new Set(["ar", "fa", "he"])
|
||||
|
||||
// activates locale
|
||||
function activateLocale(locale: string, messages: Messages = enMessages) {
|
||||
i18n.load(locale, messages)
|
||||
i18n.activate(locale)
|
||||
document.documentElement.lang = locale
|
||||
localStorage.setItem("lang", locale)
|
||||
$direction.set(locale.startsWith("ar") || locale.startsWith("fa") ? "rtl" : "ltr")
|
||||
$direction.set(rtlLanguages.has(locale) ? "rtl" : "ltr")
|
||||
}
|
||||
|
||||
// dynamically loads translations for the given locale
|
||||
|
||||
@@ -44,6 +44,11 @@ export default [
|
||||
label: "Français",
|
||||
e: "🇫🇷",
|
||||
},
|
||||
{
|
||||
lang: "he",
|
||||
label: "עברית",
|
||||
e: "🕎",
|
||||
},
|
||||
{
|
||||
lang: "hr",
|
||||
label: "Hrvatski",
|
||||
|
||||
28
internal/site/src/lib/shiki.ts
Normal file
28
internal/site/src/lib/shiki.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// https://shiki.style/guide/bundles#fine-grained-bundle
|
||||
|
||||
// directly import the theme and language modules, only the ones you imported will be bundled.
|
||||
import githubDarkDimmed from '@shikijs/themes/github-dark-dimmed'
|
||||
|
||||
// `shiki/core` entry does not include any themes or languages or the wasm binary.
|
||||
import { createHighlighterCore } from 'shiki/core'
|
||||
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
|
||||
|
||||
export const highlighter = await createHighlighterCore({
|
||||
themes: [
|
||||
// instead of strings, you need to pass the imported module
|
||||
githubDarkDimmed,
|
||||
// or a dynamic import if you want to do chunk splitting
|
||||
// import('@shikijs/themes/material-theme-ocean')
|
||||
],
|
||||
langs: [
|
||||
import('@shikijs/langs/log'),
|
||||
import('@shikijs/langs/json'),
|
||||
// shiki will try to interop the module with the default export
|
||||
// () => import('@shikijs/langs/css'),
|
||||
],
|
||||
// `shiki/wasm` contains the wasm binary inlined as base64 string.
|
||||
engine: createOnigurumaEngine(import('shiki/wasm'))
|
||||
})
|
||||
|
||||
// optionally, load themes and languages after creation
|
||||
// await highlighter.loadTheme(import('@shikijs/themes/vitesse-light'))
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
$pausedSystems,
|
||||
$upSystems,
|
||||
} from "@/lib/stores"
|
||||
import { FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED, updateFavicon } from "@/lib/utils"
|
||||
import { updateFavicon } from "@/lib/utils"
|
||||
import type { SystemRecord } from "@/types"
|
||||
import { SystemStatus } from "./enums"
|
||||
|
||||
@@ -74,9 +74,7 @@ export function init() {
|
||||
|
||||
/** Update the longest system name length and favicon based on system status */
|
||||
function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: SystemRecord | undefined) {
|
||||
const upSystemsStore = $upSystems.get()
|
||||
const downSystemsStore = $downSystems.get()
|
||||
const upSystems = Object.values(upSystemsStore)
|
||||
const downSystems = Object.values(downSystemsStore)
|
||||
|
||||
// Update longest system name length
|
||||
@@ -86,14 +84,7 @@ function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: System
|
||||
$longestSystemNameLen.set(nameLen)
|
||||
}
|
||||
|
||||
// Update favicon based on system status
|
||||
if (downSystems.length > 0) {
|
||||
updateFavicon(FAVICON_RED)
|
||||
} else if (upSystems.length > 0) {
|
||||
updateFavicon(FAVICON_GREEN)
|
||||
} else {
|
||||
updateFavicon(FAVICON_DEFAULT)
|
||||
}
|
||||
updateFavicon(downSystems.length)
|
||||
}
|
||||
|
||||
/** Fetch systems from collection */
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { plural, t } from "@lingui/core/macro"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { listenKeys } from "nanostores"
|
||||
import { timeDay, timeHour, timeMinute } from "d3-time"
|
||||
import { useEffect, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
|
||||
import { HourFormat, MeterState, Unit } from "./enums"
|
||||
import { $copyContent, $userSettings } from "./stores"
|
||||
|
||||
export const FAVICON_DEFAULT = "favicon.svg"
|
||||
export const FAVICON_GREEN = "favicon-green.svg"
|
||||
export const FAVICON_RED = "favicon-red.svg"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -100,14 +95,40 @@ export const formatDay = (timestamp: string) => {
|
||||
return dayFormatter.format(new Date(timestamp))
|
||||
}
|
||||
|
||||
export const updateFavicon = (newIcon: string) => {
|
||||
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = prependBasePath(`/static/${newIcon}`)
|
||||
}
|
||||
export const updateFavicon = (() => {
|
||||
let prevDownCount = 0
|
||||
return (downCount = 0) => {
|
||||
if (downCount === prevDownCount) {
|
||||
return
|
||||
}
|
||||
prevDownCount = downCount
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 70">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="20%" x2="100%" y2="120%">
|
||||
<stop offset="0%" style="stop-color:#747bff"/>
|
||||
<stop offset="100%" style="stop-color:#24eb5c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#gradient)" d="M35 70H0V0h35q4.4 0 8.2 1.7a21.4 21.4 0 0 1 6.6 4.5q2.9 2.8 4.5 6.6Q56 16.7 56 21a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5Q53 39.1 54 41a18.3 18.3 0 0 1 1.5 4 17.4 17.4 0 0 1 .5 3 15.3 15.3 0 0 1 0 1q0 4.4-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.6Q39.4 70 35 70ZM14 14v14h21a7 7 0 0 0 2.3-.3 6.6 6.6 0 0 0 .4-.2Q39 27 40 26a6.9 6.9 0 0 0 1.5-2.2q.5-1.3.5-2.8a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 17 40 16a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm0 28v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 54.9 40 54a7 7 0 0 0 1.5-2.2 6.9 6.9 0 0 0 .5-2.6 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 45 40 44a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Z"/>
|
||||
${downCount > 0 &&
|
||||
`
|
||||
<circle cx="40" cy="50" r="22" fill="#f00"/>
|
||||
<text x="40" y="60" font-size="34" text-anchor="middle" fill="#fff" font-family="Arial" font-weight="bold">${downCount}</text>
|
||||
`
|
||||
}
|
||||
</svg>
|
||||
`
|
||||
const blob = new Blob([svg], { type: "image/svg+xml" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
; (document.querySelector("link[rel='icon']") as HTMLLinkElement).href = url
|
||||
}
|
||||
})()
|
||||
|
||||
export const chartTimeData: ChartTimeData = {
|
||||
"1m": {
|
||||
type: "1m",
|
||||
expectedInterval: 1000,
|
||||
expectedInterval: 2000, // allow a bit of latency for one second updates (#1247)
|
||||
label: () => t`1 minute`,
|
||||
format: (timestamp: string) => hourWithSeconds(timestamp),
|
||||
ticks: 3,
|
||||
@@ -266,7 +287,7 @@ export function formatBytes(
|
||||
}
|
||||
}
|
||||
|
||||
export const chartMargin = { top: 12 }
|
||||
export const chartMargin = { top: 12, right: 5 }
|
||||
|
||||
/**
|
||||
* Retuns value of system host, truncating full path if socket.
|
||||
@@ -345,6 +366,12 @@ export function formatDuration(
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/** Parse semver string into major, minor, and patch numbers
|
||||
* @example
|
||||
* const semVer = "1.2.3"
|
||||
* const { major, minor, patch } = parseSemVer(semVer)
|
||||
* console.log(major, minor, patch) // 1, 2, 3
|
||||
*/
|
||||
export const parseSemVer = (semVer = ""): SemVer => {
|
||||
// if (semVer.startsWith("v")) {
|
||||
// semVer = semVer.slice(1)
|
||||
@@ -401,3 +428,17 @@ export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
|
||||
return state.result
|
||||
}) as T
|
||||
}
|
||||
|
||||
/** Format seconds to hours, minutes, or seconds */
|
||||
export function secondsToString(seconds: number, unit: "hour" | "minute" | "day"): string {
|
||||
const count = Math.floor(seconds / (unit === "hour" ? 3600 : unit === "minute" ? 60 : 86400))
|
||||
const countString = count.toLocaleString()
|
||||
switch (unit) {
|
||||
case "minute":
|
||||
return plural(count, { one: `${countString} minute`, few: `${countString} minutes`, many: `${countString} minutes`, other: `${countString} minutes` })
|
||||
case "hour":
|
||||
return plural(count, { one: `${countString} hour`, other: `${countString} hours` })
|
||||
case "day":
|
||||
return plural(count, { one: `${countString} day`, other: `${countString} days` })
|
||||
}
|
||||
}
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: ar\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-30 21:52\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"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: ar\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 "{0, plural, one {# يوم} other {# أيام}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# ساعة} other {# ساعات}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# دقيقة} few {# دقائق} many {# دقيقة} other {# دقيقة}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# دقيقة} few {# دقائق} many {# دقيقة} ot
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "تم تحديد {0} من {1} صف"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} يوم} other {{countString} أيام}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} ساعة} other {{countString} ساعات}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} دقيقة} few {{countString} دقائق} many {{countString} دقيقة} other {{countString} دقيقة}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 ساعة"
|
||||
@@ -89,7 +86,7 @@ msgstr "إجراءات"
|
||||
msgid "Active"
|
||||
msgstr "نشط"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "التنبيهات النشطة"
|
||||
|
||||
@@ -133,7 +130,15 @@ msgstr "سجل التنبيهات"
|
||||
msgid "Alerts"
|
||||
msgstr "التنبيهات"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "جميع الحاويات"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -226,6 +231,10 @@ msgstr "ذاكرة التخزين المؤقت / المخازن المؤقتة"
|
||||
msgid "Cancel"
|
||||
msgstr "إلغاء"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "السعة"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "تحذير - فقدان محتمل للبيانات"
|
||||
@@ -267,6 +276,14 @@ msgstr "تحقق من السجلات لمزيد من التفاصيل."
|
||||
msgid "Check your notification service"
|
||||
msgstr "تحقق من خدمة الإشعارات الخاصة بك"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "انقر على حاوية لعرض مزيد من المعلومات."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "انقر على جهاز لعرض مزيد من المعلومات."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "انقر على نظام لعرض مزيد من المعلومات."
|
||||
@@ -289,7 +306,7 @@ msgstr "هيئ التنبيهات الواردة"
|
||||
msgid "Confirm password"
|
||||
msgstr "تأكيد كلمة المرور"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "الاتصال مقطوع"
|
||||
|
||||
@@ -348,12 +365,22 @@ msgstr "انسخ محتوى <0>docker-compose.yml</0> للوكيل أدناه،
|
||||
msgid "Copy YAML"
|
||||
msgstr "نسخ YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "المعالج"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "نوى المعالج"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "تفصيل وقت المعالج"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "استخدام وحدة المعالجة المركزية"
|
||||
@@ -384,8 +411,12 @@ msgstr "الرفع التراكمي"
|
||||
msgid "Current state"
|
||||
msgstr "الحالة الحالية"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "الدورات"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "لوحة التحكم"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "حذف"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "حذف البصمة"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "التفاصيل"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "الجهاز"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -508,7 +547,7 @@ msgstr "خطأ"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "يتجاوز {0}{1} في آخر {2, plural, one {# دقيقة} other {# دقائق}}"
|
||||
|
||||
@@ -532,6 +571,10 @@ msgstr "تصدير تكوين الأنظمة الحالية الخاصة بك."
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "فهرنهايت (°ف)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "السمات الفاشلة:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "فشل في المصادقة"
|
||||
@@ -549,8 +592,10 @@ msgstr "فشل في إرسال إشعار الاختبار"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "فشل في تحديث التنبيه"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "تصفية..."
|
||||
@@ -559,6 +604,10 @@ msgstr "تصفية..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "البصمة"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "البرمجيات الثابتة"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "استهلاك طاقة وحدة معالجة الرسوميات"
|
||||
msgid "Grid"
|
||||
msgstr "شبكة"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "الصحة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "خاملة"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "إذا فقدت كلمة المرور لحساب المسؤول الخاص بك، يمكنك إعادة تعيينها باستخدام الأمر التالي."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "صورة"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "عنوان البريد الإشباكي غير صالح."
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "فشل محاولة تسجيل الدخول"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "السجلات"
|
||||
@@ -689,6 +748,7 @@ msgstr "تعليمات الإعداد اليدوي"
|
||||
msgid "Max 1 min"
|
||||
msgstr "الحد الأقصى دقيقة"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "الذاكرة"
|
||||
@@ -702,11 +762,17 @@ msgstr "استخدام الذاكرة"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "استخدام الذاكرة لحاويات دوكر"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "الموديل"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "الاسم"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "الشبكة"
|
||||
@@ -731,10 +797,18 @@ msgstr "وحدة الشبكة"
|
||||
msgid "No results found."
|
||||
msgstr "لم يتم العثور على نتائج."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "لا توجد نتائج."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "لا توجد سمات S.M.A.R.T. متاحة لهذا الجهاز."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "فتح القائمة"
|
||||
msgid "Or continue with"
|
||||
msgstr "أو المتابعة باستخدام"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "أخرى"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "الكتابة فوق التنبيهات الحالية"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "صفحة"
|
||||
@@ -815,6 +894,15 @@ msgstr "متوقف مؤقتا"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "متوقف مؤقتا ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "متوسط الاستخدام لكل نواة"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "النسبة المئوية للوقت المقضي في كل حالة"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "يرجى <0>تكوين خادم SMTP</0> لضمان تسليم التنبيهات."
|
||||
@@ -852,6 +940,11 @@ msgstr "يرجى تسجيل الدخول إلى حسابك"
|
||||
msgid "Port"
|
||||
msgstr "المنفذ"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "تشغيل الطاقة"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "قراءة"
|
||||
msgid "Received"
|
||||
msgstr "تم الاستلام"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "تحديث"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "طلب كلمة مرور لمرة واحدة"
|
||||
@@ -906,6 +1004,14 @@ msgstr "تدوير الرمز المميز"
|
||||
msgid "Rows per page"
|
||||
msgstr "صفوف لكل صفحة"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "تفاصيل S.M.A.R.T."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "اختبار S.M.A.R.T. الذاتي"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي."
|
||||
@@ -935,6 +1041,10 @@ msgstr "راجع <0>إعدادات الإشعارات</0> لتكوين كيفي
|
||||
msgid "Sent"
|
||||
msgstr "تم الإرسال"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "الرقم التسلسلي"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "تعيين عتبات النسبة المئوية لألوان العداد."
|
||||
@@ -967,6 +1077,8 @@ msgstr "الترتيب حسب"
|
||||
msgid "State"
|
||||
msgstr "الحالة"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "استخدام التبديل"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "جدول"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "درجة الحرارة"
|
||||
@@ -1129,6 +1243,10 @@ msgstr "يتم التفعيل عندما يتغير الحالة بين التش
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "النوع"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "قيد التشغيل"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "قيد التشغيل ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "تم التحديث"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "رفع"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "مدة التشغيل"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "الاستخدام"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "القيمة"
|
||||
msgid "View"
|
||||
msgstr "عرض"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "عرض المزيد"
|
||||
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: bg\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-20 21:37\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: bg\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 "{0, plural, one {# ден} other {# дни}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# час} other {# часа}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# минута} few {# минути} many {# минути} other {# минути}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# минута} few {# минути} many {# минут
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} от {1} селектирани."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} ден} other {{countString} дни}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} час} other {{countString} часа}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} минута} few {{countString} минути} many {{countString} минути} other {{countString} минути}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 час"
|
||||
@@ -89,7 +86,7 @@ msgstr "Действия"
|
||||
msgid "Active"
|
||||
msgstr "Активен"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Активни тревоги"
|
||||
|
||||
@@ -133,7 +130,15 @@ msgstr "История на нотификациите"
|
||||
msgid "Alerts"
|
||||
msgstr "Тревоги"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Всички контейнери"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -226,6 +231,10 @@ msgstr "Кеш / Буфери"
|
||||
msgid "Cancel"
|
||||
msgstr "Откажи"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Капацитет"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Внимание - възможност за загуба на данни"
|
||||
@@ -267,6 +276,14 @@ msgstr "Провери log-овете за повече информация."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Провери услугата си за удостоверяване"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Кликнете върху контейнер, за да видите повече информация."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Кликнете върху устройство, за да видите повече информация."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Кликнете върху система, за да видите повече информация."
|
||||
@@ -289,7 +306,7 @@ msgstr "Настрой как получаваш нотификации за т
|
||||
msgid "Confirm password"
|
||||
msgstr "Потвърди парола"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "Връзката е прекъсната"
|
||||
|
||||
@@ -348,12 +365,22 @@ msgstr "Копирайте съдържанието на<0>docker-compose.yml</0
|
||||
msgid "Copy YAML"
|
||||
msgstr "Копирай YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Процесор"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU ядра"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "Разбивка на времето на CPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "Употреба на процесор"
|
||||
@@ -384,8 +411,12 @@ msgstr "Кумулативно качване"
|
||||
msgid "Current state"
|
||||
msgstr "Текущо състояние"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Цикли"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Табло"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "Изтрий"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Изтрий пръстов отпечатък"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Подробности"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Устройство"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -508,7 +547,7 @@ msgstr "Грешка"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Надвишава {0}{1} в последните {2, plural, one {# минута} other {# минути}}"
|
||||
|
||||
@@ -532,6 +571,10 @@ msgstr "Експортирай конфигурацията на системи
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Фаренхайт (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Неуспешни атрибути:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Неуспешно удостоверяване"
|
||||
@@ -549,8 +592,10 @@ msgstr "Неуспешно изпрати тестова нотификация"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Неуспешно обнови тревога"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Филтрирай..."
|
||||
@@ -559,6 +604,10 @@ msgstr "Филтрирай..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Пръстов отпечатък"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Фърмуер"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "Консумация на ток от графична карта"
|
||||
msgid "Grid"
|
||||
msgstr "Мрежово"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Здраве"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Неактивна"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Ако си загубил паролата до администраторския акаунт, можеш да я нулираш със следващата команда."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Образ"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Невалиден имейл адрес."
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Неуспешен опит за вход"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Логове"
|
||||
@@ -689,6 +748,7 @@ msgstr "Инструкции за ръчна настройка"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Максимум 1 минута"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Памет"
|
||||
@@ -702,11 +762,17 @@ msgstr "Употреба на паметта"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Използването на памет от docker контейнерите"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Модел"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Име"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Мрежа"
|
||||
@@ -731,10 +797,18 @@ msgstr "Единица за измерване на скорост"
|
||||
msgid "No results found."
|
||||
msgstr "Няма намерени резултати."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "Няма резултати."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Няма налични S.M.A.R.T. атрибути за това устройство."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "Отвори менюто"
|
||||
msgid "Or continue with"
|
||||
msgstr "Или продължи с"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Други"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Презапиши съществуващи тревоги"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Страница"
|
||||
@@ -815,6 +894,15 @@ msgstr "На пауза"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "На пауза ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Средно използване на ядро"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Процент време, прекарано във всяко състояние"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Моля <0>конфигурурай SMTP сървър</0> за да се подсигуриш, че тревогите са доставени."
|
||||
@@ -852,6 +940,11 @@ msgstr "Моля влез в акаунта ти"
|
||||
msgid "Port"
|
||||
msgstr "Порт"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Включване"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Прочети"
|
||||
msgid "Received"
|
||||
msgstr "Получени"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Опресни"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Заявка за еднократна парола"
|
||||
@@ -906,6 +1004,14 @@ msgstr "Пресъздаване на идентификатора"
|
||||
msgid "Rows per page"
|
||||
msgstr "Редове на страница"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T. Детайли"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. Самотест"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл."
|
||||
@@ -935,6 +1041,10 @@ msgstr "Виж <0>настройките за нотификациите</0> з
|
||||
msgid "Sent"
|
||||
msgstr "Изпратени"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Сериен номер"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Задайте процентни прагове за цветовете на измервателните уреди."
|
||||
@@ -967,6 +1077,8 @@ msgstr "Сортиране по"
|
||||
msgid "State"
|
||||
msgstr "Състояние"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Използване на swap"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Таблица"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Температура"
|
||||
@@ -1129,6 +1243,10 @@ msgstr "Задейства се, когато статуса превключв
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Задейства се, когато употребата на някой диск надивши зададен праг"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Тип"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "Нагоре"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "Нагоре ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Актуализирано"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Качване"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Време на работа"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Употреба"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "Стойност"
|
||||
msgid "View"
|
||||
msgstr "Изглед"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Виж повече"
|
||||
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: cs\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-28 23:00\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: cs\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 "{0, plural, one {# den} few {# dny} other {# dní}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} z {1} vybraných řádků."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} den} few {{countString} dny} other {{countString} dní}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} Hodina} few {{countString} Hodiny} many {{countString} Hodin} other {{countString} Hodin}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minuta} few {{countString} minuty} many {{countString} minut} other {{countString} minut}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 hodina"
|
||||
@@ -46,7 +43,7 @@ msgstr "1 hodina"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "1 min"
|
||||
msgstr "1 min"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 minute"
|
||||
@@ -63,7 +60,7 @@ msgstr "12 hodin"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "15 min"
|
||||
msgstr "15 min"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "24 hours"
|
||||
@@ -76,7 +73,7 @@ msgstr "30 dní"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "5 min"
|
||||
msgstr "5 min"
|
||||
msgstr ""
|
||||
|
||||
#. Table column
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -89,7 +86,7 @@ msgstr "Akce"
|
||||
msgid "Active"
|
||||
msgstr "Aktivní"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Aktivní výstrahy"
|
||||
|
||||
@@ -116,11 +113,11 @@ msgstr "Upravit možnosti zobrazení pro grafy."
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
msgstr "Administrátor"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
msgstr "Agent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
@@ -133,7 +130,15 @@ msgstr "Historie upozornění"
|
||||
msgid "Alerts"
|
||||
msgstr "Výstrahy"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Všechny kontejnery"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -205,17 +210,17 @@ msgstr "Beszel používá <0>Shoutrrr</0> k integraci s populárními notifikač
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Binary"
|
||||
msgstr "Binary"
|
||||
msgstr "Binární"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr "Bity (Kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr "Byty (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
@@ -226,6 +231,10 @@ msgstr "Cache / vyrovnávací paměť"
|
||||
msgid "Cancel"
|
||||
msgstr "Zrušit"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Kapacita"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Upozornění - možná ztráta dat"
|
||||
@@ -267,6 +276,14 @@ msgstr "Pro více informací zkontrolujte logy."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Zkontrolujte službu upozornění"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Klikněte na kontejner pro zobrazení dalších informací."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Klikněte na zařízení pro zobrazení dalších informací."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Klikněte na systém pro zobrazení více informací."
|
||||
@@ -289,7 +306,7 @@ msgstr "Konfigurace způsobu přijímání upozornění."
|
||||
msgid "Confirm password"
|
||||
msgstr "Potvrdit heslo"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "Připojení je nedostupné"
|
||||
|
||||
@@ -348,12 +365,22 @@ msgstr "Zkopírujte obsah <0>docker-compose.yml</0> pro agenta níže nebo autom
|
||||
msgid "Copy YAML"
|
||||
msgstr "Kopírovat YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Procesor"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU jádra"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "Rozdělení času CPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "Využití procesoru"
|
||||
@@ -384,8 +411,12 @@ msgstr "Kumulativní odeslání"
|
||||
msgid "Current state"
|
||||
msgstr "Aktuální stav"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Cykly"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Přehled"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "Odstranit"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Smazat identifikátor"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Zařízení"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -409,11 +448,11 @@ msgstr "Vybíjení"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Disk I/O"
|
||||
msgstr "Disk I/O"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Disk unit"
|
||||
@@ -474,7 +513,7 @@ msgstr "Upravit"
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Email notifications"
|
||||
@@ -508,7 +547,7 @@ msgstr "Chyba"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Překračuje {0}{1} za {2, plural, one {poslední # minutu} few {poslední # minuty} other {posledních # minut}}"
|
||||
|
||||
@@ -518,7 +557,7 @@ msgstr "Stávající systémy, které nejsou definovány v <0>config.yml</0>, bu
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
msgstr "Exportovat"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
@@ -532,6 +571,10 @@ msgstr "Exportovat aktuální konfiguraci systémů."
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheita (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Neúspěšné atributy:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Ověření se nezdařilo"
|
||||
@@ -549,8 +592,10 @@ msgstr "Nepodařilo se odeslat testovací oznámení"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Nepodařilo se aktualizovat upozornění"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filtr..."
|
||||
@@ -559,6 +604,10 @@ msgstr "Filtr..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Otisk"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-sheet.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}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "Spotřeba energie GPU"
|
||||
msgid "Grid"
|
||||
msgstr "Mřížka"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Zdraví"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Neaktivní"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Pokud jste ztratili heslo k vašemu účtu správce, můžete jej obnovit pomocí následujícího příkazu."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Obraz"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Neplatná e-mailová adresa."
|
||||
@@ -622,7 +680,7 @@ msgstr "Neplatná e-mailová adresa."
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
msgstr "Jádro"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Pokus o přihlášení selhal"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Logy"
|
||||
@@ -689,6 +748,7 @@ msgstr "Pokyny k manuálnímu nastavení"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max. 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Paměť"
|
||||
@@ -702,11 +762,17 @@ msgstr "Využití paměti"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Využití paměti docker kontejnerů"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Název"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Síť"
|
||||
@@ -731,10 +797,18 @@ msgstr "Síťová jednotka"
|
||||
msgid "No results found."
|
||||
msgstr "Nenalezeny žádné výskyty."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "Žádné výsledky."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Pro toto zařízení nejsou k dispozici žádné atributy S.M.A.R.T."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "Otevřít menu"
|
||||
msgid "Or continue with"
|
||||
msgstr "Nebo pokračujte s"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Jiné"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Přepsat existující upozornění"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Stránka"
|
||||
@@ -815,6 +894,15 @@ msgstr "Pozastaveno"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "Pozastaveno ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Průměrné využití na jádro"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Procento času strávěného v každém stavu"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "<0>nakonfigurujte SMTP server</0> pro zajištění toho, aby byla upozornění doručena."
|
||||
@@ -850,7 +938,12 @@ msgstr "Přihlaste se prosím k vašemu účtu"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
msgstr ""
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Zapnutí"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -876,6 +969,11 @@ msgstr "Číst"
|
||||
msgid "Received"
|
||||
msgstr "Přijato"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Aktualizovat"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Požádat o jednorázové heslo"
|
||||
@@ -906,6 +1004,14 @@ msgstr "Změnit token"
|
||||
msgid "Rows per page"
|
||||
msgstr "Řádků na stránku"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T. Detaily"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. Vlastní test"
|
||||
|
||||
#: 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."
|
||||
@@ -935,6 +1041,10 @@ msgstr "Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak
|
||||
msgid "Sent"
|
||||
msgstr "Odeslat"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Sériové číslo"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Nastavte procentuální prahové hodnoty pro barvy měřičů."
|
||||
@@ -967,6 +1077,8 @@ msgstr "Seřadit podle"
|
||||
msgid "State"
|
||||
msgstr "Stav"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Swap využití"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Tabulka"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Teplota"
|
||||
@@ -1023,7 +1137,7 @@ msgstr "Teploty systémových senzorů"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Test <0>URL</0>"
|
||||
msgstr "Test <0>URL</0>"
|
||||
msgstr "Testovat <0>URL</0>"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Test notification sent"
|
||||
@@ -1069,7 +1183,7 @@ msgstr "Přepnout motiv"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -1129,6 +1243,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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Typ"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "Funkční"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "Funkční ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Aktualizováno"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Odeslání"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Doba provozu"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Využití"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "Hodnota"
|
||||
msgid "View"
|
||||
msgstr "Zobrazení"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Zobrazit více"
|
||||
|
||||
@@ -8,36 +8,33 @@ msgstr ""
|
||||
"Language: da\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-28 23:00\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: da\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 "{0, plural, one {# day} other {# days}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# hour} other {# hours}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr ""
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr ""
|
||||
msgstr "{0} af {1} række(r) valgt."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} dag} other {{countString} dage}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} time} other {{countString} timer}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minut} other {{countString} minutter}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
@@ -46,7 +43,7 @@ msgstr "1 time"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "1 min"
|
||||
msgstr ""
|
||||
msgstr "1 minut"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 minute"
|
||||
@@ -63,7 +60,7 @@ msgstr "12 timer"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "15 min"
|
||||
msgstr ""
|
||||
msgstr "15 minutter"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "24 hours"
|
||||
@@ -76,7 +73,7 @@ msgstr "30 dage"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "5 min"
|
||||
msgstr ""
|
||||
msgstr "5 minutter"
|
||||
|
||||
#. Table column
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -87,9 +84,9 @@ msgstr "Handlinger"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Aktive Alarmer"
|
||||
|
||||
@@ -116,7 +113,7 @@ msgstr "Juster visningsindstillinger for diagrammer."
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
@@ -126,14 +123,22 @@ msgstr "Agent"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Alert History"
|
||||
msgstr ""
|
||||
msgstr "Advarselshistorik"
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Alerts"
|
||||
msgstr "Alarmer"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Alle containere"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -145,7 +150,7 @@ msgstr "Er du sikker på, at du vil slette {name}?"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
msgstr "Er du sikker?"
|
||||
|
||||
#: src/components/copy-to-clipboard.tsx
|
||||
msgid "Automatic copy requires a secure context."
|
||||
@@ -179,7 +184,7 @@ msgstr "Gennemsnitlig udnyttelse af {0}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of GPU engines"
|
||||
msgstr "Gennemsnitlig udnyttelse af GPU-motorer"
|
||||
msgstr "Gennemsnitlig udnyttelse af GPU-enheder"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
@@ -210,12 +215,12 @@ msgstr "Binær"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr ""
|
||||
msgstr "Bits (Kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr ""
|
||||
msgstr "Bytes (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
@@ -226,17 +231,21 @@ msgstr "Cache / Buffere"
|
||||
msgid "Cancel"
|
||||
msgstr "Fortryd"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Kapacitet"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Forsigtig - muligt tab af data"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Celsius (°C)"
|
||||
msgstr ""
|
||||
msgstr "Celsius (°C)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change display units for metrics."
|
||||
msgstr ""
|
||||
msgstr "Ændre viste enheder for målinger."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change general application options."
|
||||
@@ -267,9 +276,17 @@ msgstr "Tjek logfiler for flere detaljer."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Tjek din notifikationstjeneste"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Klik på en container for at se mere information."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Klik på en enhed for at se flere oplysninger."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr ""
|
||||
msgstr "Klik på et system for at se mere information."
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
@@ -289,9 +306,9 @@ msgstr "Konfigurer hvordan du modtager advarselsmeddelelser."
|
||||
msgid "Confirm password"
|
||||
msgstr "Bekræft adgangskode"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr ""
|
||||
msgstr "Forbindelsen er nede"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -317,7 +334,7 @@ msgstr "Kopiér docker run"
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
msgstr "Kopier miljø"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Copy host"
|
||||
@@ -338,22 +355,32 @@ 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 ""
|
||||
msgstr "Kopier installationskommandoen for agenten nedenfor, eller registrer agenter automatisk med en <0>universalnøgle</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 ""
|
||||
msgstr "Kopier <0>docker-compose.yml</0> indholdet for agenten nedenfor, eller registrer agenter automatisk med en <1>universalnøgle</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
msgstr "Kopier YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU-kerner"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "CPU-tidsfordeling"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "CPU forbrug"
|
||||
@@ -365,7 +392,7 @@ msgstr "Opret konto"
|
||||
#. Context: date created
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
msgstr "Oprettet"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Critical (%)"
|
||||
@@ -384,8 +411,12 @@ msgstr "Kumulativ upload"
|
||||
msgid "Current state"
|
||||
msgstr "Nuværende tilstand"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Cykler"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Oversigtspanel"
|
||||
|
||||
@@ -400,7 +431,15 @@ msgstr "Slet"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
msgstr "Slet fingeraftryk"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Detalje"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Enhed"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -417,7 +456,7 @@ msgstr "Disk I/O"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Disk unit"
|
||||
msgstr ""
|
||||
msgstr "Diskenhed"
|
||||
|
||||
#: src/components/charts/disk-chart.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -455,15 +494,15 @@ msgstr "Nede"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Nede ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
msgstr "Hent ned"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
msgstr "Varighed"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -508,7 +547,7 @@ msgstr "Fejl"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Overskrider {0}{1} i sidste {2, plural, one {# minut} other {# minutter}}"
|
||||
|
||||
@@ -518,7 +557,7 @@ msgstr "Eksisterende systemer ikke defineret i <0>config.yml</0> vil blive slett
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
msgstr "Eksporter"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
@@ -530,7 +569,11 @@ msgstr "Eksporter din nuværende systemkonfiguration."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Mislykkede attributter:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
@@ -549,14 +592,20 @@ msgstr "Afsendelse af testnotifikation mislykkedes"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Kunne ikke opdatere alarm"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Fingeraftryk"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
@@ -586,7 +635,7 @@ msgstr "Generelt"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Engines"
|
||||
msgstr "GPU-motorer"
|
||||
msgstr "GPU-enheder"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
@@ -596,6 +645,10 @@ msgstr "Gpu Strøm Træk"
|
||||
msgid "Grid"
|
||||
msgstr "Gitter"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Sundhed"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Inaktiv"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Hvis du har mistet adgangskoden til din administratorkonto, kan du nulstille den ved hjælp af følgende kommando."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Billede"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ugyldig email adresse."
|
||||
@@ -622,7 +680,7 @@ msgstr "Ugyldig email adresse."
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
msgstr "Kerne"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
@@ -630,28 +688,28 @@ msgstr "Sprog"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Layout"
|
||||
msgstr "Layout"
|
||||
msgstr "Opstilling"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
msgstr ""
|
||||
msgstr "Belastning Gennemsnitlig"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
msgstr "Belastning Gennemsnitlig 15m"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 1m"
|
||||
msgstr ""
|
||||
msgstr "Belastning Gennemsnitlig 1m"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
msgstr "Belastning Gennemsnitlig 5m"
|
||||
|
||||
#. Short label for load average
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Load Avg"
|
||||
msgstr ""
|
||||
msgstr "Belastning gns."
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Loginforsøg mislykkedes"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
@@ -689,6 +748,7 @@ msgstr "Manuel opsætningsvejledning"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Maks. 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Hukommelse"
|
||||
@@ -702,14 +762,20 @@ msgstr "Hukommelsesforbrug"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Hukommelsesforbrug af dockercontainere"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Model"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Navn"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
@@ -725,15 +791,23 @@ msgstr "Netværkstrafik af offentlige grænseflader"
|
||||
#. Context: Bytes or bits
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Network unit"
|
||||
msgstr ""
|
||||
msgstr "Netværksenhed"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "No results found."
|
||||
msgstr "Ingen resultater fundet."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr ""
|
||||
msgstr "Ingen resultater."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Ingen S.M.A.R.T.-attributter tilgængelige for denne enhed."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
@@ -768,10 +842,15 @@ msgstr "Åbn menu"
|
||||
msgid "Or continue with"
|
||||
msgstr "Eller fortsæt med"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Andre"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Overskriv eksisterende alarmer"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Side"
|
||||
@@ -780,7 +859,7 @@ msgstr "Side"
|
||||
#. placeholder {1}: table.getPageCount()
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Page {0} of {1}"
|
||||
msgstr ""
|
||||
msgstr "Side {0} af {1}"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Pages / Settings"
|
||||
@@ -813,7 +892,16 @@ msgstr "Sat på pause"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Sat på pause ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Gennemsnitlig udnyttelse pr. kerne"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Procentdel af tid brugt i hver tilstand"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -852,6 +940,11 @@ msgstr "Log venligst ind på din konto"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Tænd"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Læs"
|
||||
msgid "Received"
|
||||
msgstr "Modtaget"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Opdater"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Anmod om engangsadgangskode"
|
||||
@@ -892,7 +990,7 @@ msgstr "Nulstil adgangskode"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Resolved"
|
||||
msgstr ""
|
||||
msgstr "Løst"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Resume"
|
||||
@@ -900,11 +998,19 @@ msgstr "Genoptag"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
msgstr "Roter nøgle"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Rows per page"
|
||||
msgstr ""
|
||||
msgstr "Rækker per side"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T.-detaljer"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. selvtest"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
@@ -935,6 +1041,10 @@ msgstr "Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtag
|
||||
msgid "Sent"
|
||||
msgstr "Sendt"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Serienummer"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Indstil procentvise tærskler for målerfarver."
|
||||
@@ -965,8 +1075,10 @@ msgstr "Sorter efter"
|
||||
#. Context: alert state (active or resolved)
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
msgstr "Tilstand"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Swap forbrug"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -989,7 +1102,7 @@ msgstr "System"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "System load averages over time"
|
||||
msgstr ""
|
||||
msgstr "Gennemsnitlig system belastning over tid"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Tabel"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Temperatur"
|
||||
@@ -1015,7 +1129,7 @@ msgstr "Temperatur"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Temperature unit"
|
||||
msgstr ""
|
||||
msgstr "Temperaturenhed"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Temperatures of system sensors"
|
||||
@@ -1039,7 +1153,7 @@ msgstr "Denne handling kan ikke fortrydes. Dette vil permanent slette alle aktue
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "This will permanently delete all selected records from the database."
|
||||
msgstr ""
|
||||
msgstr "Dette vil permanent slette alle poster fra databasen."
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Throughput of {extraFsName}"
|
||||
@@ -1069,21 +1183,21 @@ msgstr "Skift tema"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
msgstr "Nøgle"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
msgstr "Nøgler & fingeraftryk"
|
||||
|
||||
#: 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 ""
|
||||
msgstr "Nøgler tillader agenter at oprette forbindelse og registrere. Fingeraftryk er stabile identifikatorer unikke for hvert system, indstillet ved første forbindelse."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
msgstr "Nøgler og fingeraftryk bruges til at godkende WebSocket-forbindelser til hubben."
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data received for each interface"
|
||||
@@ -1095,15 +1209,15 @@ msgstr "Samlet sendt data for hver interface"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Udløser når 1 minut belastning gennemsnit overstiger en tærskel"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Udløser når 15 minut belastning gennemsnit overstiger en tærskel"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Udløser når 5 minut belastning gennemsnit overstiger en tærskel"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
@@ -1129,15 +1243,19 @@ 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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
msgstr ""
|
||||
msgstr "Enhedspræferencer"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
msgstr "Universalnøgle"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -1152,11 +1270,15 @@ msgstr "Oppe"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Oppe ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Opdateret"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Upload"
|
||||
msgstr "Overfør"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Uptime"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Oppetid"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Forbrug"
|
||||
|
||||
@@ -1185,19 +1308,20 @@ msgstr "Brugere"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
msgstr "Værdi"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "View"
|
||||
msgstr "Vis"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Se mere"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "View your 200 most recent alerts."
|
||||
msgstr ""
|
||||
msgstr "Se dine 200 nyeste alarmer."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Visible Fields"
|
||||
@@ -1225,7 +1349,7 @@ 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 ""
|
||||
msgstr "Når aktiveret tillader denne nøgle agenter at selvregistrere uden forudgående systemoprettelse. Udløber efter en time eller ved hub-genstart."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-10-05 16:13\n"
|
||||
"PO-Revision-Date: 2025-10-28 22:59\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: de\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 "{0, plural, one {# Tag} other {# Tage}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# Stunde} other {# Stunden}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# Minute} other {# Minuten}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# Minute} other {# Minuten}}"
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} von {1} Zeile(n) ausgewählt."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} Tag} other {{countString} Tage}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} Stunde} other {{countString} Stunden}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} Minute} other {{countString} Minuten}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 Stunde"
|
||||
@@ -89,7 +86,7 @@ msgstr "Aktionen"
|
||||
msgid "Active"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Aktive Warnungen"
|
||||
|
||||
@@ -126,14 +123,22 @@ msgstr "Agent"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Alert History"
|
||||
msgstr "Alarm-Verlauf"
|
||||
msgstr "Warnungsverlauf"
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Alerts"
|
||||
msgstr "Warnungen"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Alle Container"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -226,6 +231,10 @@ msgstr "Cache / Puffer"
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Kapazität"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Vorsicht - potenzieller Datenverlust"
|
||||
@@ -267,6 +276,14 @@ msgstr "Überprüfe die Protokolle für weitere Details."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Überprüfe deinen Benachrichtigungsdienst"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Klicke auf einen Container, um weitere Informationen zu sehen."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Klicke auf ein Gerät, um weitere Informationen zu sehen."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Klicke auf ein System, um weitere Informationen zu sehen."
|
||||
@@ -289,7 +306,7 @@ msgstr "Konfiguriere, wie du Warnbenachrichtigungen erhältst."
|
||||
msgid "Confirm password"
|
||||
msgstr "Passwort bestätigen"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "Verbindung unterbrochen"
|
||||
|
||||
@@ -338,22 +355,32 @@ 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>."
|
||||
msgstr "Kopiere den Installationsbefehl für den Agent unten oder registriere 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>."
|
||||
msgstr "Kopiere den<0>docker-compose.yml</0> Inhalt für den Agent unten oder registriere Agents automatisch mit einem <1>universellen Token</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "YAML kopieren"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU-Kerne"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "CPU-Zeit-Aufschlüsselung"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "CPU-Auslastung"
|
||||
@@ -384,8 +411,12 @@ msgstr "Kumulativer Upload"
|
||||
msgid "Current state"
|
||||
msgstr "Aktueller Zustand"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Zyklen"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "Löschen"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Fingerabdruck löschen"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Details"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -508,7 +547,7 @@ msgstr "Fehler"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Überschreitet {0}{1} in den letzten {2, plural, one {# Minute} other {# Minuten}}"
|
||||
|
||||
@@ -532,6 +571,10 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Fehlgeschlagene Attribute:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Authentifizierung fehlgeschlagen"
|
||||
@@ -549,8 +592,10 @@ msgstr "Testbenachrichtigung konnte nicht gesendet werden"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Warnung konnte nicht aktualisiert werden"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
@@ -559,6 +604,10 @@ msgstr "Filter..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Fingerabdruck"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "GPU-Leistungsaufnahme"
|
||||
msgid "Grid"
|
||||
msgstr "Raster"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Gesundheit"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Untätig"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Wenn du das Passwort für dein Administratorkonto verloren hast, kannst du es mit dem folgenden Befehl zurücksetzen."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Image"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ungültige E-Mail-Adresse."
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Anmeldeversuch fehlgeschlagen"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Protokolle"
|
||||
@@ -689,6 +748,7 @@ msgstr "Anleitung zur manuellen Einrichtung"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max 1 Min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Arbeitsspeicher"
|
||||
@@ -702,11 +762,17 @@ msgstr "Arbeitsspeichernutzung"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Arbeitsspeichernutzung der Docker-Container"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Modell"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Netz"
|
||||
@@ -731,10 +797,18 @@ msgstr "Netzwerkeinheit"
|
||||
msgid "No results found."
|
||||
msgstr "Keine Ergebnisse gefunden."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "Keine Ergebnisse."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Für dieses Gerät sind keine S.M.A.R.T.-Attribute verfügbar."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "Menü öffnen"
|
||||
msgid "Or continue with"
|
||||
msgstr "Oder fortfahren mit"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Andere"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Bestehende Warnungen überschreiben"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Seite"
|
||||
@@ -815,6 +894,15 @@ msgstr "Pausiert"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "Pausiert ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Durchschnittliche Auslastung pro Kern"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Prozentsatz der Zeit in jedem Zustand"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Bitte <0>konfiguriere einen SMTP-Server</0>, um sicherzustellen, dass Warnungen zugestellt werden."
|
||||
@@ -852,6 +940,11 @@ msgstr "Bitte melde dich bei deinem Konto an"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Eingeschaltet"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -864,7 +957,7 @@ msgstr "Bevorzugte Sprache"
|
||||
#. Use 'Key' if your language requires many more characters
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Public Key"
|
||||
msgstr "Schlüssel"
|
||||
msgstr "Öffentlicher Schlüssel"
|
||||
|
||||
#. Disk read
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -876,6 +969,11 @@ msgstr "Lesen"
|
||||
msgid "Received"
|
||||
msgstr "Empfangen"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Aktualisieren"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Einmalpasswort anfordern"
|
||||
@@ -906,6 +1004,14 @@ msgstr "Token rotieren"
|
||||
msgid "Rows per page"
|
||||
msgstr "Zeilen pro Seite"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T.-Details"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T.-Selbsttest"
|
||||
|
||||
#: 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."
|
||||
@@ -917,7 +1023,7 @@ msgstr "Einstellungen speichern"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr "System sichern"
|
||||
msgstr "System speichern"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -935,6 +1041,10 @@ msgstr "Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du
|
||||
msgid "Sent"
|
||||
msgstr "Gesendet"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Seriennummer"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Prozentuale Schwellenwerte für Zählerfarben festlegen."
|
||||
@@ -967,6 +1077,8 @@ msgstr "Sortieren nach"
|
||||
msgid "State"
|
||||
msgstr "Status"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Swap-Nutzung"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Tabelle"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Temperatur"
|
||||
@@ -1087,11 +1201,11 @@ msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen z
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data received for each interface"
|
||||
msgstr "Gesamtdatenmenge für jede Schnittstelle empfangen"
|
||||
msgstr "Empfangene Gesamtdatenmenge je Schnittstelle "
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data sent for each interface"
|
||||
msgstr "Gesamtdatenmenge für jede Schnittstelle gesendet"
|
||||
msgstr "Gesendete Gesamtdatenmenge je Schnittstelle"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
@@ -1111,7 +1225,7 @@ msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Löst aus, wenn die kombinierte Auf-/Abwärtsbewegung einen Schwellenwert überschreitet"
|
||||
msgstr "Löst aus, wenn die kombinierte Up- und Downloadrate einen Schwellenwert überschreitet"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when CPU usage exceeds a threshold"
|
||||
@@ -1129,6 +1243,10 @@ 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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Typ"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "aktiv"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "aktiv ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Aktualisiert"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Hochladen"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Betriebszeit"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Nutzung"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "Wert"
|
||||
msgid "View"
|
||||
msgstr "Ansicht"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Mehr anzeigen"
|
||||
|
||||
1296
internal/site/src/locales/el/el.po
Normal file
1296
internal/site/src/locales/el/el.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,27 +13,24 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# day} other {# days}}"
|
||||
msgstr "{0, plural, one {# day} other {# days}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# hour} other {# hours}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} of {1} row(s) selected."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 hour"
|
||||
@@ -84,7 +81,7 @@ msgstr "Actions"
|
||||
msgid "Active"
|
||||
msgstr "Active"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Active Alerts"
|
||||
|
||||
@@ -128,7 +125,15 @@ msgstr "Alert History"
|
||||
msgid "Alerts"
|
||||
msgstr "Alerts"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "All Containers"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -221,6 +226,10 @@ msgstr "Cache / Buffers"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Capacity"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Caution - potential data loss"
|
||||
@@ -262,6 +271,14 @@ msgstr "Check logs for more details."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Check your notification service"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Click on a container to view more information."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Click on a device to view more information."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Click on a system to view more information."
|
||||
@@ -284,7 +301,7 @@ msgstr "Configure how you receive alert notifications."
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirm password"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "Connection is down"
|
||||
|
||||
@@ -343,12 +360,22 @@ msgstr "Copy the<0>docker-compose.yml</0> content for the agent below, or regist
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copy YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU Cores"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "CPU Time Breakdown"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "CPU Usage"
|
||||
@@ -379,8 +406,12 @@ msgstr "Cumulative Upload"
|
||||
msgid "Current state"
|
||||
msgstr "Current state"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Cycles"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
@@ -397,6 +428,14 @@ msgstr "Delete"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Delete fingerprint"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Detail"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Device"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -503,7 +542,7 @@ msgstr "Error"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
|
||||
@@ -527,6 +566,10 @@ msgstr "Export your current systems configuration."
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Failed Attributes:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Failed to authenticate"
|
||||
@@ -544,8 +587,10 @@ msgstr "Failed to send test notification"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Failed to update alert"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
@@ -554,6 +599,10 @@ msgstr "Filter..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Fingerprint"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -591,6 +640,10 @@ msgstr "GPU Power Draw"
|
||||
msgid "Grid"
|
||||
msgstr "Grid"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Health"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -610,6 +663,11 @@ msgstr "Idle"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Image"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Invalid email address."
|
||||
@@ -662,6 +720,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Login attempt failed"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
@@ -684,6 +743,7 @@ msgstr "Manual setup instructions"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Memory"
|
||||
@@ -697,11 +757,17 @@ msgstr "Memory Usage"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Memory usage of docker containers"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Model"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
@@ -726,10 +792,18 @@ msgstr "Network unit"
|
||||
msgid "No results found."
|
||||
msgstr "No results found."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "No results."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "No S.M.A.R.T. attributes available for this device."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -763,10 +837,15 @@ msgstr "Open menu"
|
||||
msgid "Or continue with"
|
||||
msgstr "Or continue with"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Other"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Overwrite existing alerts"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Page"
|
||||
@@ -810,6 +889,15 @@ msgstr "Paused"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "Paused ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Per-core average utilization"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Percentage of time spent in each state"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -847,6 +935,11 @@ msgstr "Please sign in to your account"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Power On"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -871,6 +964,11 @@ msgstr "Read"
|
||||
msgid "Received"
|
||||
msgstr "Received"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Refresh"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Request a one-time password"
|
||||
@@ -901,6 +999,14 @@ msgstr "Rotate token"
|
||||
msgid "Rows per page"
|
||||
msgstr "Rows per page"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T. Details"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. Self-Test"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
@@ -930,6 +1036,10 @@ msgstr "See <0>notification settings</0> to configure how you receive alerts."
|
||||
msgid "Sent"
|
||||
msgstr "Sent"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Serial Number"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Set percentage thresholds for meter colors."
|
||||
@@ -962,6 +1072,8 @@ msgstr "Sort By"
|
||||
msgid "State"
|
||||
msgstr "State"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -976,6 +1088,7 @@ msgid "Swap Usage"
|
||||
msgstr "Swap Usage"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -999,6 +1112,7 @@ msgid "Table"
|
||||
msgstr "Table"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Temp"
|
||||
@@ -1124,6 +1238,10 @@ msgstr "Triggers when status switches between up and down"
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Triggers when usage of any disk exceeds a threshold"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1149,6 +1267,10 @@ msgstr "Up"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "Up ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Updated"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Upload"
|
||||
@@ -1161,6 +1283,7 @@ msgstr "Uptime"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Usage"
|
||||
|
||||
@@ -1186,6 +1309,7 @@ msgstr "Value"
|
||||
msgid "View"
|
||||
msgstr "View"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "View more"
|
||||
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-11-01 17:41\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: es-ES\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 "{0, plural, one {# día} other {# días}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# hora} other {# horas}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# minuto} other {# minutos}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# minuto} other {# minutos}}"
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} de {1} fila(s) seleccionada(s)."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} día} other {{countString} días}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} hora} other {{countString} horas}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minuto} other {{countString} minutos}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 hora"
|
||||
@@ -89,17 +86,17 @@ msgstr "Acciones"
|
||||
msgid "Active"
|
||||
msgstr "Activo"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Alertas Activas"
|
||||
msgstr "Alertas activas"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
msgstr "Agregar <0>Sistema</0>"
|
||||
msgstr "Agregar <0>sistema</0>"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add New System"
|
||||
msgstr "Agregar Nuevo Sistema"
|
||||
msgstr "Agregar nuevo sistema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add system"
|
||||
@@ -133,7 +130,15 @@ msgstr "Historial de Alertas"
|
||||
msgid "Alerts"
|
||||
msgstr "Alertas"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Todos los contenedores"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -141,7 +146,7 @@ msgstr "Todos los Sistemas"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Are you sure you want to delete {name}?"
|
||||
msgstr "¿Está seguro de que desea eliminar {name}?"
|
||||
msgstr "¿Estás seguro de que deseas eliminar {name}?"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Are you sure?"
|
||||
@@ -184,7 +189,7 @@ msgstr "Utilización promedio de motores GPU"
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Backups"
|
||||
msgstr "Copias de Seguridad"
|
||||
msgstr "Copias de seguridad"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -210,12 +215,12 @@ msgstr "Binario"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr "Bits (kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr "Bytes (kB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
@@ -226,6 +231,10 @@ msgstr "Caché / Buffers"
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Capacidad"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Precaución - posible pérdida de datos"
|
||||
@@ -253,27 +262,35 @@ msgstr "Cargando"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Chart options"
|
||||
msgstr "Opciones de Gráficos"
|
||||
msgstr "Opciones de gráficos"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Check {email} for a reset link."
|
||||
msgstr "Revise {email} para un enlace de restablecimiento."
|
||||
msgstr "Revisa {email} para un enlace de restablecimiento."
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Check logs for more details."
|
||||
msgstr "Revise los registros para más detalles."
|
||||
msgstr "Revisa los registros para más detalles."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Check your notification service"
|
||||
msgstr "Verifique su servicio de notificaciones"
|
||||
msgstr "Verifica tu servicio de notificaciones"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Haga clic en un contenedor para ver más información."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Haz clic en un dispositivo para ver más información."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Haga clic en un sistema para ver más información."
|
||||
msgstr "Haz clic en un sistema para ver más información."
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
msgstr "Haga clic para copiar"
|
||||
msgstr "Haz clic para copiar"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
@@ -282,14 +299,14 @@ msgstr "Instrucciones de línea de comandos"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Configure how you receive alert notifications."
|
||||
msgstr "Configure cómo recibe las notificaciones de alertas."
|
||||
msgstr "Configura cómo recibe las notificaciones de alertas."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirmar contraseña"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "La conexión está caída"
|
||||
|
||||
@@ -348,12 +365,22 @@ msgstr "Copia el contenido del<0>docker-compose.yml</0> para el agente a continu
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copiar YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "Núcleos de CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "Desglose de tiempo de CPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "Uso de CPU"
|
||||
@@ -365,7 +392,7 @@ msgstr "Crear cuenta"
|
||||
#. Context: date created
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Created"
|
||||
msgstr "Creado"
|
||||
msgstr "Creada"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Critical (%)"
|
||||
@@ -373,25 +400,29 @@ msgstr "Crítico (%)"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Cumulative Download"
|
||||
msgstr "Descarga acumulativa"
|
||||
msgstr "Descarga acumulada"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Cumulative Upload"
|
||||
msgstr "Carga acumulativa"
|
||||
msgstr "Carga acumulada"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Current state"
|
||||
msgstr "Estado actual"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Ciclos"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Tablero"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Default time period"
|
||||
msgstr "Período de tiempo predeterminado"
|
||||
msgstr "Periodo de tiempo predeterminado"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -402,6 +433,14 @@ msgstr "Eliminar"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Eliminar huella digital"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Detalle"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -423,7 +462,7 @@ msgstr "Unidad de disco"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Disk Usage"
|
||||
msgstr "Uso de Disco"
|
||||
msgstr "Uso de disco"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Disk usage of {extraFsName}"
|
||||
@@ -435,11 +474,11 @@ msgstr "Uso de CPU de Docker"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Memory Usage"
|
||||
msgstr "Uso de Memoria de Docker"
|
||||
msgstr "Uso de memoria de Docker"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Network I/O"
|
||||
msgstr "E/S de Red de Docker"
|
||||
msgstr "E/S de red de Docker"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Documentation"
|
||||
@@ -451,11 +490,11 @@ msgstr "Documentación"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
msgstr "Abajo"
|
||||
msgstr "Caído"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr "Abajo ({downSystemsLength})"
|
||||
msgstr "Caído ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
@@ -487,11 +526,11 @@ msgstr "Vacía"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Enter email address to reset password"
|
||||
msgstr "Ingrese la dirección de correo electrónico para restablecer la contraseña"
|
||||
msgstr "Ingresa la dirección de correo electrónico para restablecer la contraseña"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Enter email address..."
|
||||
msgstr "Ingrese dirección de correo..."
|
||||
msgstr "Ingresa dirección de correo..."
|
||||
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Enter your one-time password."
|
||||
@@ -508,13 +547,13 @@ msgstr "Error"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Excede {0}{1} en el último {2, plural, one {# minuto} other {# minutos}}"
|
||||
|
||||
#: 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 "Los sistemas existentes no definidos en <0>config.yml</0> serán eliminados. Por favor, haga copias de seguridad regularmente."
|
||||
msgstr "Los sistemas existentes no definidos en <0>config.yml</0> serán eliminados. Por favor, haz copias de seguridad regularmente."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
@@ -526,12 +565,16 @@ msgstr "Exportar configuración"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export your current systems configuration."
|
||||
msgstr "Exporte la configuración actual de sus sistemas."
|
||||
msgstr "Exporta la configuración actual de sus sistemas."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Atributos fallidos:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Error al autenticar"
|
||||
@@ -549,8 +592,10 @@ msgstr "Error al enviar la notificación de prueba"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Error al actualizar la alerta"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrar..."
|
||||
@@ -559,13 +604,17 @@ msgstr "Filtrar..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Huella dactilar"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "¿Olvidó su contraseña?"
|
||||
msgstr "¿Olvidaste tu contraseña?"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -596,6 +645,10 @@ msgstr "Consumo de energía de la GPU"
|
||||
msgid "Grid"
|
||||
msgstr "Cuadrícula"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Estado"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -604,7 +657,7 @@ msgstr "Comando Homebrew"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
msgstr "Host / IP"
|
||||
msgstr "Servidor / IP"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -613,7 +666,12 @@ msgstr "Inactiva"
|
||||
|
||||
#: 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 "Si ha perdido la contraseña de su cuenta de administrador, puede restablecerla usando el siguiente comando."
|
||||
msgstr "Si has perdido la contraseña de tu cuenta de administrador, puedes restablecerla usando el siguiente comando."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Imagen"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
@@ -634,7 +692,7 @@ msgstr "Diseño"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
msgstr "Carga Media"
|
||||
msgstr "Carga media"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 15m"
|
||||
@@ -655,7 +713,7 @@ msgstr "Carga media"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Cerrar Sesión"
|
||||
msgstr "Cerrar sesión"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Login"
|
||||
@@ -667,13 +725,14 @@ msgid "Login attempt failed"
|
||||
msgstr "Intento de inicio de sesión fallido"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Registros"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
msgstr "¿Busca dónde crear alertas? Haga clic en los iconos de campana <0/> en la tabla de sistemas."
|
||||
msgstr "¿Buscas dónde crear alertas? Haz clic en los iconos de campana <0/> en la tabla de sistemas."
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Manage display and notification preferences."
|
||||
@@ -689,6 +748,7 @@ msgstr "Instrucciones manuales de configuración"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Máx 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Memoria"
|
||||
@@ -702,11 +762,17 @@ msgstr "Uso de Memoria"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Uso de memoria de los contenedores de Docker"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Modelo"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Red"
|
||||
@@ -731,10 +797,18 @@ msgstr "Unidad de red"
|
||||
msgid "No results found."
|
||||
msgstr "No se encontraron resultados."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "Sin resultados."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "No hay atributos S.M.A.R.T. disponibles para este dispositivo."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "Abrir menú"
|
||||
msgid "Or continue with"
|
||||
msgstr "O continuar con"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Otro"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Sobrescribir alertas existentes"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Página"
|
||||
@@ -815,6 +894,15 @@ msgstr "Pausado"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "Pausado ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Utilización promedio por núcleo"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Porcentaje de tiempo dedicado a cada estado"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Por favor, <0>configure un servidor SMTP</0> para asegurar que las alertas sean entregadas."
|
||||
@@ -852,6 +940,11 @@ msgstr "Por favor, inicie sesión en su cuenta"
|
||||
msgid "Port"
|
||||
msgstr "Puerto"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Encendido"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Lectura"
|
||||
msgid "Received"
|
||||
msgstr "Recibido"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Actualizar"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Solicitar contraseña de un solo uso"
|
||||
@@ -906,6 +1004,14 @@ msgstr "Rotar token"
|
||||
msgid "Rows per page"
|
||||
msgstr "Filas por página"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "Detalles S.M.A.R.T."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "Autoprueba S.M.A.R.T."
|
||||
|
||||
#: 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."
|
||||
@@ -913,11 +1019,11 @@ msgstr "Guarde la dirección usando la tecla enter o coma. Deje en blanco para d
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save Settings"
|
||||
msgstr "Guardar Configuración"
|
||||
msgstr "Guardar configuración"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr "Guardar Sistema"
|
||||
msgstr "Guardar sistema"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -935,6 +1041,10 @@ msgstr "Consulte <0>configuración de notificaciones</0> para configurar cómo r
|
||||
msgid "Sent"
|
||||
msgstr "Enviado"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Número de serie"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Establecer umbrales de porcentaje para los colores de los medidores."
|
||||
@@ -967,6 +1077,8 @@ msgstr "Ordenar por"
|
||||
msgid "State"
|
||||
msgstr "Estado"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Uso de Swap"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Tabla"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Temperatura"
|
||||
@@ -1129,6 +1243,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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Tipo"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "Activo"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "Activo ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Actualizado"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Cargar"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Tiempo de actividad"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Uso"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "Valor"
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Ver más"
|
||||
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: fa\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-28 23:00\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Persian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: fa\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 "{0, plural, one {# روز} other {# روز}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# ساعت} other {# ساعت}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# دقیقه} few {# دقیقه} many {# دقیقه} other {# دقیقه}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# دقیقه} few {# دقیقه} many {# دقیقه} ot
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} از {1} ردیف انتخاب شده است."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} روز} other {{countString} روز}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} ساعت} other {{countString} ساعت}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} دقیقه} few {{countString} دقیقه} many {{countString} دقیقه} other {{countString} دقیقه}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "۱ ساعت"
|
||||
@@ -89,7 +86,7 @@ msgstr "عملیات"
|
||||
msgid "Active"
|
||||
msgstr "فعال"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr " هشدارهای فعال"
|
||||
|
||||
@@ -133,7 +130,15 @@ msgstr "تاریخچه هشدارها"
|
||||
msgid "Alerts"
|
||||
msgstr "هشدارها"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "همه کانتینرها"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -226,6 +231,10 @@ msgstr "حافظه پنهان / بافرها"
|
||||
msgid "Cancel"
|
||||
msgstr "لغو"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "ظرفیت"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "احتیاط - احتمال از دست رفتن دادهها"
|
||||
@@ -267,6 +276,14 @@ msgstr "برای جزئیات بیشتر، لاگها را بررسی کنی
|
||||
msgid "Check your notification service"
|
||||
msgstr "سرویس اطلاعرسانی خود را بررسی کنید"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "برای مشاهده اطلاعات بیشتر روی کانتینر کلیک کنید."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "برای مشاهده اطلاعات بیشتر روی دستگاه کلیک کنید."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "برای مشاهده اطلاعات بیشتر روی یک سیستم کلیک کنید."
|
||||
@@ -289,7 +306,7 @@ msgstr "نحوه دریافت هشدارهای اطلاعرسانی را پی
|
||||
msgid "Confirm password"
|
||||
msgstr "تأیید رمز عبور"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "اتصال قطع است"
|
||||
|
||||
@@ -348,12 +365,22 @@ msgstr "محتوای <0>docker-compose.yml</0> عامل زیر را کپی کن
|
||||
msgid "Copy YAML"
|
||||
msgstr "کپی YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "پردازنده"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "هستههای CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "تجزیه زمان CPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "میزان استفاده از پردازنده"
|
||||
@@ -384,8 +411,12 @@ msgstr "آپلود تجمعی"
|
||||
msgid "Current state"
|
||||
msgstr "وضعیت فعلی"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "چرخهها"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "داشبورد"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "حذف"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "حذف اثر انگشت"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "جزئیات"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "دستگاه"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -508,7 +547,7 @@ msgstr "خطا"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "در {2, plural, one {# دقیقه} other {# دقیقه}} گذشته از {0}{1} بیشتر است"
|
||||
|
||||
@@ -532,6 +571,10 @@ msgstr "پیکربندی سیستمهای فعلی خود را خارج کن
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "فارنهایت (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "ویژگیهای ناموفق:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "احراز هویت ناموفق بود"
|
||||
@@ -549,8 +592,10 @@ msgstr "ارسال اعلان آزمایشی ناموفق بود"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "بهروزرسانی هشدار ناموفق بود"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "فیلتر..."
|
||||
@@ -559,6 +604,10 @@ msgstr "فیلتر..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "اثر انگشت"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "فرمویر"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "برای <0>{min}</0> {min, plural, one {دقیقه} other {دقیقه}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "مصرف برق پردازنده گرافیکی"
|
||||
msgid "Grid"
|
||||
msgstr "جدول"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "سلامتی"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "بیکار"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "اگر رمز عبور حساب مدیر خود را گم کردهاید، میتوانید آن را با استفاده از دستور زیر بازنشانی کنید."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "تصویر"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "آدرس ایمیل نامعتبر است."
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "تلاش برای ورود ناموفق بود"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "لاگها"
|
||||
@@ -689,6 +748,7 @@ msgstr "دستورالعملهای راهاندازی دستی"
|
||||
msgid "Max 1 min"
|
||||
msgstr "حداکثر ۱ دقیقه"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "حافظه"
|
||||
@@ -702,11 +762,17 @@ msgstr "میزان استفاده از حافظه"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "میزان استفاده از حافظه کانتینرهای داکر"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "مدل"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "نام"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "شبکه"
|
||||
@@ -731,10 +797,18 @@ msgstr "واحد شبکه"
|
||||
msgid "No results found."
|
||||
msgstr "هیچ نتیجهای یافت نشد."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "نتیجهای یافت نشد."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "هیچ ویژگی S.M.A.R.T برای این دستگاه موجود نیست."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "باز کردن منو"
|
||||
msgid "Or continue with"
|
||||
msgstr "یا ادامه با"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "سایر"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "بازنویسی هشدارهای موجود"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "صفحه"
|
||||
@@ -815,6 +894,15 @@ msgstr "مکث شده"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "مکث شده ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "میانگین استفاده در هر هسته"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "درصد زمان صرف شده در هر حالت"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "لطفاً برای اطمینان از تحویل هشدارها، یک <0>سرور SMTP پیکربندی کنید</0>."
|
||||
@@ -852,6 +940,11 @@ msgstr "لطفاً به حساب کاربری خود وارد شوید"
|
||||
msgid "Port"
|
||||
msgstr "پورت"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "روشن کردن"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "خواندن"
|
||||
msgid "Received"
|
||||
msgstr "دریافت شد"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "تازهسازی"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "درخواست رمز عبور یکبار مصرف"
|
||||
@@ -906,6 +1004,14 @@ msgstr "چرخش توکن"
|
||||
msgid "Rows per page"
|
||||
msgstr "ردیف در هر صفحه"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "جزئیات S.M.A.R.T"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "تست خود S.M.A.R.T"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "آدرس را با استفاده از کلید Enter یا کاما ذخیره کنید. برای غیرفعال کردن اعلانهای ایمیلی، خالی بگذارید."
|
||||
@@ -935,6 +1041,10 @@ msgstr "برای پیکربندی نحوه دریافت هشدارها، به <0
|
||||
msgid "Sent"
|
||||
msgstr "ارسال شد"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "شماره سریال"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "آستانه های درصدی را برای رنگ های متر تنظیم کنید."
|
||||
@@ -967,6 +1077,8 @@ msgstr "مرتبسازی بر اساس"
|
||||
msgid "State"
|
||||
msgstr "وضعیت"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "میزان استفاده از Swap"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "جدول"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "دما"
|
||||
@@ -1129,6 +1243,10 @@ msgstr "هنگامی که وضعیت بین بالا و پایین تغییر م
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "هنگامی که استفاده از هر دیسکی از یک آستانه فراتر رود، فعال میشود"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "نوع"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "فعال"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "فعال ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "بهروزرسانی شد"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "آپلود"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "آپتایم"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "میزان استفاده"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "مقدار"
|
||||
msgid "View"
|
||||
msgstr "مشاهده"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "مشاهده بیشتر"
|
||||
|
||||
@@ -8,36 +8,33 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-28 22:59\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: fr\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 "{0, plural, one {# jour} other {# jours}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# heure} other {# heures}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr ""
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr ""
|
||||
msgstr "{0} sur {1} ligne(s) sélectionnée(s)."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} jour} other {{countString} jours}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} heure} other {{countString} heures}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
@@ -46,7 +43,7 @@ msgstr "1 heure"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "1 min"
|
||||
msgstr ""
|
||||
msgstr "1 min"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 minute"
|
||||
@@ -63,7 +60,7 @@ msgstr "12 heures"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "15 min"
|
||||
msgstr ""
|
||||
msgstr "15 min"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "24 hours"
|
||||
@@ -76,7 +73,7 @@ msgstr "30 jours"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "5 min"
|
||||
msgstr ""
|
||||
msgstr "5 min"
|
||||
|
||||
#. Table column
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -87,9 +84,9 @@ msgstr "Actions"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Active"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Alertes actives"
|
||||
|
||||
@@ -126,14 +123,22 @@ msgstr "Agent"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Alert History"
|
||||
msgstr ""
|
||||
msgstr "Historique des alertes"
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Alerts"
|
||||
msgstr "Alertes"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Tous les conteneurs"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -145,7 +150,7 @@ msgstr "Êtes-vous sûr de vouloir supprimer {name} ?"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
msgstr "Êtes-vous sûr ?"
|
||||
|
||||
#: src/components/copy-to-clipboard.tsx
|
||||
msgid "Automatic copy requires a secure context."
|
||||
@@ -210,12 +215,12 @@ msgstr "Binaire"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr ""
|
||||
msgstr "Bits (Kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr ""
|
||||
msgstr "Bytes (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
@@ -226,17 +231,21 @@ msgstr "Cache / Tampons"
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Capacité"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Attention - perte de données potentielle"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Celsius (°C)"
|
||||
msgstr ""
|
||||
msgstr "Celsius (°C)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change display units for metrics."
|
||||
msgstr ""
|
||||
msgstr "Ajuster les unités d'affichage pour les métriques."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change general application options."
|
||||
@@ -267,9 +276,17 @@ msgstr "Vérifiez les journaux pour plus de détails."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Vérifiez votre service de notification"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Cliquez sur un conteneur pour voir plus d'informations."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Cliquez sur un appareil pour voir plus d'informations."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr ""
|
||||
msgstr "Cliquez sur un système pour voir plus d'informations."
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
@@ -289,9 +306,9 @@ msgstr "Configurez comment vous recevez les notifications d'alerte."
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirmer le mot de passe"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr ""
|
||||
msgstr "Connexion interrompue"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -348,12 +365,22 @@ msgstr "Copiez le contenu du<0>docker-compose.yml</0> pour l'agent ci-dessous, o
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copier YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "Cœurs CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "Répartition du temps CPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "Utilisation du CPU"
|
||||
@@ -365,7 +392,7 @@ msgstr "Créer un compte"
|
||||
#. Context: date created
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
msgstr "Date de création"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Critical (%)"
|
||||
@@ -384,8 +411,12 @@ msgstr "Téléversement cumulatif"
|
||||
msgid "Current state"
|
||||
msgstr "État actuel"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Tableau de bord"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "Supprimer"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Supprimer l'empreinte"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Détail"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Appareil"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -417,7 +456,7 @@ msgstr "Entrée/Sortie disque"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Disk unit"
|
||||
msgstr ""
|
||||
msgstr "Unité disque"
|
||||
|
||||
#: src/components/charts/disk-chart.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -455,7 +494,7 @@ msgstr "Injoignable"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Injoignable ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
@@ -463,7 +502,7 @@ msgstr "Télécharger"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
msgstr "Durée"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -508,7 +547,7 @@ msgstr "Erreur"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Dépasse {0}{1} dans {2, plural, one {la dernière # minute} other {les dernières # minutes}}"
|
||||
|
||||
@@ -518,7 +557,7 @@ msgstr "Les systèmes existants non définis dans <0>config.yml</0> seront suppr
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
msgstr "Exporter"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
@@ -530,7 +569,11 @@ msgstr "Exportez la configuration actuelle de vos systèmes."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Attributs défaillants :"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
@@ -549,15 +592,21 @@ msgstr "Échec de l'envoi de la notification de test"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Échec de la mise à jour de l'alerte"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrer..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
msgstr "Empreinte"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Micrologiciel"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "Consommation du GPU"
|
||||
msgid "Grid"
|
||||
msgstr "Grille"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Santé"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Inactive"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Si vous avez perdu le mot de passe de votre compte administrateur, vous pouvez le réinitialiser en utilisant la commande suivante."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Image"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Adresse email invalide."
|
||||
@@ -634,24 +692,24 @@ msgstr "Disposition"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
msgstr ""
|
||||
msgstr "Charge moyenne"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
msgstr "Charge moyenne 15m"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 1m"
|
||||
msgstr ""
|
||||
msgstr "Charge moyenne 1m"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
msgstr "Charge moyenne 5m"
|
||||
|
||||
#. Short label for load average
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Load Avg"
|
||||
msgstr ""
|
||||
msgstr "Charge moy."
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Échec de la tentative de connexion"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Journaux"
|
||||
@@ -689,6 +748,7 @@ msgstr "Guide pour une installation manuelle"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Mémoire"
|
||||
@@ -702,11 +762,17 @@ msgstr "Utilisation de la mémoire"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Utilisation de la mémoire des conteneurs Docker"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
@@ -725,15 +791,23 @@ msgstr "Trafic réseau des interfaces publiques"
|
||||
#. Context: Bytes or bits
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Network unit"
|
||||
msgstr ""
|
||||
msgstr "Unité réseau"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "No results found."
|
||||
msgstr "Aucun résultat trouvé."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr ""
|
||||
msgstr "Aucun résultat."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Aucun attribut S.M.A.R.T. disponible pour cet appareil."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
@@ -768,10 +842,15 @@ msgstr "Ouvrir le menu"
|
||||
msgid "Or continue with"
|
||||
msgstr "Ou continuer avec"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Autre"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Écraser les alertes existantes"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Page"
|
||||
@@ -780,7 +859,7 @@ msgstr "Page"
|
||||
#. placeholder {1}: table.getPageCount()
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Page {0} of {1}"
|
||||
msgstr ""
|
||||
msgstr "Page {0} sur {1}"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Pages / Settings"
|
||||
@@ -813,7 +892,16 @@ msgstr "En pause"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Mis en pause ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Utilisation moyenne par cœur"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Pourcentage de temps passé dans chaque état"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -852,6 +940,11 @@ msgstr "Veuillez vous connecter à votre compte"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Allumage"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Lecture"
|
||||
msgid "Received"
|
||||
msgstr "Reçu"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Actualiser"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Demander un mot de passe à usage unique"
|
||||
@@ -892,7 +990,7 @@ msgstr "Réinitialiser le mot de passe"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Resolved"
|
||||
msgstr ""
|
||||
msgstr "Résolution"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Resume"
|
||||
@@ -904,7 +1002,15 @@ msgstr "Faire tourner le token"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Rows per page"
|
||||
msgstr ""
|
||||
msgstr "Lignes par page"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "Détails S.M.A.R.T."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "Auto-test S.M.A.R.T."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
@@ -935,6 +1041,10 @@ msgstr "Voir les <0>paramètres de notification</0> pour configurer comment vous
|
||||
msgid "Sent"
|
||||
msgstr "Envoyé"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Numéro de série"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Définir des seuils de pourcentage pour les couleurs des compteurs."
|
||||
@@ -965,8 +1075,10 @@ msgstr "Trier par"
|
||||
#. Context: alert state (active or resolved)
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
msgstr "État"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Utilisation du swap"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -989,7 +1102,7 @@ msgstr "Système"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "System load averages over time"
|
||||
msgstr ""
|
||||
msgstr "Charges moyennes du système dans le temps"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Tableau"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Temp."
|
||||
@@ -1015,7 +1129,7 @@ msgstr "Température"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Temperature unit"
|
||||
msgstr ""
|
||||
msgstr "Unité de température"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Temperatures of system sensors"
|
||||
@@ -1039,7 +1153,7 @@ msgstr "Cette action ne peut pas être annulée. Cela supprimera définitivement
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "This will permanently delete all selected records from the database."
|
||||
msgstr ""
|
||||
msgstr "Ceci supprimera définitivement tous les enregistrements sélectionnés de la base de données."
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Throughput of {extraFsName}"
|
||||
@@ -1069,7 +1183,7 @@ msgstr "Changer le thème"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -1095,15 +1209,15 @@ msgstr "Données totales envoyées pour chaque interface"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Se déclenche lorsque la charge moyenne sur 1 minute dépasse un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Se déclenche lorsque la charge moyenne sur 15 minute dépasse un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Se déclenche lorsque la charge moyenne sur 5 minute dépasse un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
@@ -1129,10 +1243,14 @@ 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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
msgstr ""
|
||||
msgstr "Préférences des unités"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -1152,7 +1270,11 @@ msgstr "Joignable"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Joignable ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Mis à jour"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Temps de fonctionnement"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Utilisation"
|
||||
|
||||
@@ -1185,19 +1308,20 @@ msgstr "Utilisateurs"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
msgstr "Valeur"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "View"
|
||||
msgstr "Vue"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Voir plus"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "View your 200 most recent alerts."
|
||||
msgstr ""
|
||||
msgstr "Voir vos 200 dernières alertes."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Visible Fields"
|
||||
|
||||
1376
internal/site/src/locales/he/he.po
Normal file
1376
internal/site/src/locales/he/he.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-09-23 12:43\n"
|
||||
"PO-Revision-Date: 2025-10-28 23:00\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"
|
||||
@@ -18,26 +18,23 @@ msgstr ""
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# day} other {# days}}"
|
||||
msgstr "{0, plural, one {# dan} other {# dani}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# sat} other {# sati}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr ""
|
||||
msgstr "{0} od {1} redaka izabrano."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} dan} other {{countString} dani}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} sat} other {{countString} sati}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minuta} few {{countString} minuta} many {{countString} minuta} other {{countString} minute}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
@@ -76,7 +73,7 @@ msgstr "30 dana"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "5 min"
|
||||
msgstr ""
|
||||
msgstr "5 minuta"
|
||||
|
||||
#. Table column
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -89,7 +86,7 @@ msgstr "Akcije"
|
||||
msgid "Active"
|
||||
msgstr "Aktivan"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Aktivna upozorenja"
|
||||
|
||||
@@ -126,14 +123,22 @@ msgstr "Agent"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Alert History"
|
||||
msgstr ""
|
||||
msgstr "Povijest Upozorenja"
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Alerts"
|
||||
msgstr "Upozorenja"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Svi spremnici"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -166,7 +171,7 @@ msgstr "Prosjek premašuje <0>{value}{0}</0>"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average power consumption of GPUs"
|
||||
msgstr ""
|
||||
msgstr "Prosječna potrošnja energije grafičkog procesora"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average system-wide CPU utilization"
|
||||
@@ -175,7 +180,7 @@ msgstr "Prosječna iskorištenost procesora na cijelom sustavu"
|
||||
#. placeholder {0}: gpu.n
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of {0}"
|
||||
msgstr ""
|
||||
msgstr "Prosječna iskorištenost {0}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of GPU engines"
|
||||
@@ -210,12 +215,12 @@ msgstr "Binarni"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr ""
|
||||
msgstr "Bitovi (Kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr ""
|
||||
msgstr "Bajtovi (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
@@ -226,17 +231,21 @@ msgstr "Predmemorija / Međuspremnici"
|
||||
msgid "Cancel"
|
||||
msgstr "Otkaži"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Kapacitet"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Oprez - mogući gubitak podataka"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Celsius (°C)"
|
||||
msgstr ""
|
||||
msgstr "Celsius (°C)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change display units for metrics."
|
||||
msgstr ""
|
||||
msgstr "Promijenite mjerene jedinice korištene za prikazivanje podataka."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change general application options."
|
||||
@@ -267,9 +276,17 @@ msgstr "Provjerite logove za više detalja."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Provjerite Vaš servis notifikacija"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Kliknite na spremnik za prikaz više informacija."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Kliknite na uređaj da biste vidjeli više informacija."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr ""
|
||||
msgstr "Odaberite sustav za prikaz više informacija."
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
@@ -289,9 +306,9 @@ msgstr "Konfigurirajte način primanja obavijesti upozorenja."
|
||||
msgid "Confirm password"
|
||||
msgstr "Potvrdite lozinku"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr ""
|
||||
msgstr "Veza je pala"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -338,22 +355,32 @@ 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 ""
|
||||
msgstr "Kopirajte instalacijsku komandu za opisanog agenta ili automatski registrirajte agenta uz pomoć <0>sveopćeg tokena</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 ""
|
||||
msgstr "Kopirajte sadržaj <0>docker-compose.yml</0> datoteke za opisanog agenta ili automatski registrirajte agenta uz pomoć <1>sveopćeg tokena</1>."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
msgstr "Kopiraj YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Procesor"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU jezgre"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "Raspodjela CPU vremena"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "Iskorištenost procesora"
|
||||
@@ -384,8 +411,12 @@ msgstr "Kumulativno otpremanje"
|
||||
msgid "Current state"
|
||||
msgstr "Trenutno stanje"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Ciklusi"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Nadzorna ploča"
|
||||
|
||||
@@ -400,7 +431,15 @@ msgstr "Izbriši"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
msgstr "Izbriši otisak"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Detalj"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Uređaj"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -417,7 +456,7 @@ msgstr "Disk I/O"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Disk unit"
|
||||
msgstr ""
|
||||
msgstr "Mjerna jedinica za disk"
|
||||
|
||||
#: src/components/charts/disk-chart.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -451,11 +490,11 @@ msgstr "Dokumentacija"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
msgstr "Sustav je pao"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Sustav je pao ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
@@ -508,7 +547,7 @@ msgstr "Greška"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Premašuje {0}{1} u posljednjih {2, plural, one {# minuta} other {# minute}}"
|
||||
|
||||
@@ -530,7 +569,11 @@ msgstr "Izvoz trenutne sistemske konfiguracije."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
msgstr "Farenhajt (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Neuspjeli atributi:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
@@ -549,8 +592,10 @@ msgstr "Neuspješno slanje testne notifikacije"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Ažuriranje upozorenja nije uspjelo"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filtriraj..."
|
||||
@@ -559,6 +604,10 @@ msgstr "Filtriraj..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Otisak prsta"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minutu} other {minute}}"
|
||||
@@ -590,12 +639,16 @@ msgstr "GPU motori"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
msgstr ""
|
||||
msgstr "Energetska potrošnja grafičkog procesora"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Grid"
|
||||
msgstr "Mreža"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Zdravlje"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Neaktivna"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Ako ste izgubili lozinku za svoj administratorski račun, možete ju resetirati pomoću sljedeće naredbe."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Slika"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Nevažeća adresa e-pošte."
|
||||
@@ -622,7 +680,7 @@ msgstr "Nevažeća adresa e-pošte."
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
msgstr "Jezgra"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
@@ -634,19 +692,19 @@ msgstr "Izgled"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
msgstr ""
|
||||
msgstr "Prosječno Opterećenje"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
msgstr "Prosječno Opterećenje 15m"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 1m"
|
||||
msgstr ""
|
||||
msgstr "Prosječno Opterećenje 1m"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
msgstr "Prosječno Opterećenje 5m"
|
||||
|
||||
#. Short label for load average
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Pokušaj prijave nije uspio"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Logovi"
|
||||
@@ -682,13 +741,14 @@ msgstr "Upravljajte postavkama prikaza i obavijesti."
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
msgstr "Upute za ručno postavljanje"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Max 1 min"
|
||||
msgstr "Maksimalno 1 minuta"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Memorija"
|
||||
@@ -702,11 +762,17 @@ msgstr "Upotreba memorije"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Upotreba memorije Docker spremnika"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Ime"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Mreža"
|
||||
@@ -725,16 +791,24 @@ msgstr "Mrežni promet javnih sučelja"
|
||||
#. Context: Bytes or bits
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Network unit"
|
||||
msgstr ""
|
||||
msgstr "Mjerna jedinica za mrežu"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "No results found."
|
||||
msgstr "Nema rezultata."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "Nema rezultata."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Nema dostupnih S.M.A.R.T. atributa za ovaj uređaj."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "Otvori menu"
|
||||
msgid "Or continue with"
|
||||
msgstr "Ili nastavi sa"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Ostalo"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Prebrišite postojeća upozorenja"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Stranica"
|
||||
@@ -815,6 +894,15 @@ msgstr "Pauzirano"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "Pauzirano ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Prosječna iskorištenost po jezgri"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Postotak vremena provedenog u svakom stanju"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku upozorenja."
|
||||
@@ -852,6 +940,11 @@ msgstr "Molimo prijavite se u svoj račun"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Uključivanje"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Pročitaj"
|
||||
msgid "Received"
|
||||
msgstr "Primljeno"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Osvježi"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Zatraži jednokratnu lozinku"
|
||||
@@ -892,7 +990,7 @@ msgstr "Resetiraj Lozinku"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Resolved"
|
||||
msgstr ""
|
||||
msgstr "Razrješeno"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Resume"
|
||||
@@ -900,11 +998,19 @@ msgstr "Nastavi"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
msgstr "Promijeni token"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Rows per page"
|
||||
msgstr ""
|
||||
msgstr "Redovi po stranici"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T. Detalji"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. Samotestiranje"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
@@ -917,7 +1023,7 @@ msgstr "Spremi Postavke"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr ""
|
||||
msgstr "Spremi sustav"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -935,6 +1041,10 @@ msgstr "Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način prim
|
||||
msgid "Sent"
|
||||
msgstr "Poslano"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Serijski broj"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Postavite pragove postotka za boje mjerača."
|
||||
@@ -965,8 +1075,10 @@ msgstr "Sortiraj po"
|
||||
#. Context: alert state (active or resolved)
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
msgstr "Stanje"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Swap Iskorištenost"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -989,7 +1102,7 @@ msgstr "Sistem"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "System load averages over time"
|
||||
msgstr ""
|
||||
msgstr "Prosječno opterećenje sustava kroz vrijeme"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
@@ -1004,9 +1117,10 @@ msgid "Table"
|
||||
msgstr "Tablica"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
msgstr "Temp"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1015,7 +1129,7 @@ msgstr "Temperatura"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Temperature unit"
|
||||
msgstr ""
|
||||
msgstr "Mjerna jedinica za temperaturu"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Temperatures of system sensors"
|
||||
@@ -1039,7 +1153,7 @@ msgstr "Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve trenutne
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "This will permanently delete all selected records from the database."
|
||||
msgstr ""
|
||||
msgstr "Ovom radnjom će se trajno izbrisati svi odabrani zapisi iz baze podataka."
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Throughput of {extraFsName}"
|
||||
@@ -1069,21 +1183,21 @@ msgstr "Uključi/isključi temu"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
msgstr "Tokeni & Otisci"
|
||||
|
||||
#: 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 ""
|
||||
msgstr "Tokeni dopuštaju agentima prijavu i registraciju. Otisci su stabilni identifikatori jedinstveni svakom sustavu, koji se postavljaju prilikom prvog spajanja."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
msgstr "Tokeni se uz otiske koriste za autentifikaciju WebSocket veza prema središnjoj kontroli."
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data received for each interface"
|
||||
@@ -1095,15 +1209,15 @@ msgstr "Ukupni podaci poslani za svako sučelje"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Pokreće se kada prosječna opterećenost sustava unutar 1 minute prijeđe prag"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Pokreće se kada prosječna opterećenost sustava unutar 15 minuta prijeđe prag"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Pokreće se kada prosječna opterećenost sustava unutar 5 minuta prijeđe prag"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
@@ -1129,15 +1243,19 @@ 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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Vrsta"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
msgstr ""
|
||||
msgstr "Opcije mjernih jedinica"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
msgstr "Sveopći token"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -1148,11 +1266,15 @@ msgstr "Nepoznata"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
msgstr "Sustav je podignut"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Sustav je podignut ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Ažurirano"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Vrijeme rada"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Iskorištenost"
|
||||
|
||||
@@ -1191,13 +1314,14 @@ msgstr "Vrijednost"
|
||||
msgid "View"
|
||||
msgstr "Prikaz"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Prikaži više"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "View your 200 most recent alerts."
|
||||
msgstr ""
|
||||
msgstr "Pogledajte posljednjih 200 upozorenja."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Visible Fields"
|
||||
@@ -1225,7 +1349,7 @@ 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 ""
|
||||
msgstr "Kada je podešen, ovaj token dopušta agentima da se prijave bez prvobitnog stvaranja sustava. Ističe nakon jednog sata ili ponovnog pokretanja središnje kontrole."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,36 +8,33 @@ msgstr ""
|
||||
"Language: hu\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-28 22:59\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: hu\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 "{0, plural, one {# nap} other {# nap}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# óra} other {# óra}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr ""
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr ""
|
||||
msgstr "{0} a(z) {1} sorból kiválasztva."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} nap} other {{countString} nap}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} óra} other {{countString} óra}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} perc} few {{countString} perc} many {{countString} perc} other {{countString} perc}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
@@ -46,7 +43,7 @@ msgstr "1 óra"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "1 min"
|
||||
msgstr ""
|
||||
msgstr "1 perc"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 minute"
|
||||
@@ -63,7 +60,7 @@ msgstr "12 óra"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "15 min"
|
||||
msgstr ""
|
||||
msgstr "15 perc"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "24 hours"
|
||||
@@ -76,7 +73,7 @@ msgstr "30 nap"
|
||||
#. Load average
|
||||
#: src/components/charts/load-average-chart.tsx
|
||||
msgid "5 min"
|
||||
msgstr ""
|
||||
msgstr "5 perc"
|
||||
|
||||
#. Table column
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -87,9 +84,9 @@ msgstr "Műveletek"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Aktív"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Aktív riasztások"
|
||||
|
||||
@@ -116,7 +113,7 @@ msgstr "Állítsa be a diagram megjelenítését."
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
msgstr "Adminisztráció"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
@@ -126,14 +123,22 @@ msgstr "Ügynök"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Alert History"
|
||||
msgstr ""
|
||||
msgstr "Riasztási előzmények"
|
||||
|
||||
#: src/components/alerts/alert-button.tsx
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Alerts"
|
||||
msgstr "Riasztások"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Összes konténer"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -145,7 +150,7 @@ msgstr "Biztosan törölni szeretnéd {name}-t?"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
msgstr "Biztos vagy benne?"
|
||||
|
||||
#: src/components/copy-to-clipboard.tsx
|
||||
msgid "Automatic copy requires a secure context."
|
||||
@@ -179,7 +184,7 @@ msgstr "{0} átlagos kihasználtsága"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of GPU engines"
|
||||
msgstr "GPU motorok átlagos kihasználtsága"
|
||||
msgstr "GPU-k átlagos kihasználtsága"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
@@ -210,12 +215,12 @@ msgstr "Bináris"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr ""
|
||||
msgstr "Bitek (Kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr ""
|
||||
msgstr "Byte-ok (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
@@ -226,17 +231,21 @@ msgstr "Gyorsítótár / Pufferelések"
|
||||
msgid "Cancel"
|
||||
msgstr "Mégsem"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Kapacitás"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Figyelem - potenciális adatvesztés"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Celsius (°C)"
|
||||
msgstr ""
|
||||
msgstr "Celsius (°C)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change display units for metrics."
|
||||
msgstr ""
|
||||
msgstr "A mértékegységek megjelenítésének megváltoztatása."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change general application options."
|
||||
@@ -267,9 +276,17 @@ 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/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Kattintson egy konténerre a további információk megtekintéséhez."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Kattintson egy eszközre további információk megtekintéséhez."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr ""
|
||||
msgstr "További információkért kattints egy rendszerre."
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
@@ -289,9 +306,9 @@ msgstr "Konfiguráld, hogyan kapod az értesítéseket."
|
||||
msgid "Confirm password"
|
||||
msgstr "Jelszó megerősítése"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr ""
|
||||
msgstr "Kapcsolat megszakadt"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -317,7 +334,7 @@ msgstr "Docker run másolása"
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Environment variables"
|
||||
msgid "Copy env"
|
||||
msgstr ""
|
||||
msgstr "Környezet másolása"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Copy host"
|
||||
@@ -346,14 +363,24 @@ msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
msgstr "YAML másolása"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "CPU magok"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "CPU idő felbontása"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "CPU használat"
|
||||
@@ -365,7 +392,7 @@ msgstr "Fiók létrehozása"
|
||||
#. Context: date created
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
msgstr "Létrehozva"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Critical (%)"
|
||||
@@ -384,8 +411,12 @@ msgstr "Kumulatív feltöltés"
|
||||
msgid "Current state"
|
||||
msgstr "Jelenlegi állapot"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Ciklusok"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Áttekintés"
|
||||
|
||||
@@ -400,7 +431,15 @@ msgstr "Törlés"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
msgstr "Ujjlenyomat törlése"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Részlet"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Eszköz"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -417,7 +456,7 @@ msgstr "Lemez I/O"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Disk unit"
|
||||
msgstr ""
|
||||
msgstr "Lemez mértékegysége"
|
||||
|
||||
#: src/components/charts/disk-chart.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -451,11 +490,11 @@ msgstr "Dokumentáció"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
msgstr ""
|
||||
msgstr "Offline"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Offline ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
@@ -463,12 +502,12 @@ msgstr "Letöltés"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
msgstr "Időtartam"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Szerkesztés"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
@@ -508,7 +547,7 @@ msgstr "Hiba"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Túllépi a {0}{1} értéket az elmúlt {2, plural, one {# percben} other {# percben}}"
|
||||
|
||||
@@ -518,7 +557,7 @@ msgstr "A <0>config.yml</0> fájlban nem definiált meglévő rendszerek törlé
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
msgstr "Exportálás"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
@@ -530,7 +569,11 @@ msgstr "Exportálja a jelenlegi rendszerkonfigurációt."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Sikertelen attribútumok:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
@@ -549,14 +592,20 @@ msgstr "Teszt értesítés elküldése sikertelen"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Nem sikerült frissíteni a riasztást"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Szűrő..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Ujjlenyomat"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
@@ -586,7 +635,7 @@ msgstr "Általános"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Engines"
|
||||
msgstr "GPU motorok"
|
||||
msgstr "GPU-k"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
@@ -596,6 +645,10 @@ msgstr "GPU áramfelvétele"
|
||||
msgid "Grid"
|
||||
msgstr "Rács"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Egészség"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Tétlen"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Ha elvesztette az admin fiók jelszavát, a következő paranccsal állíthatja vissza."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Kép"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Érvénytelen e-mail cím."
|
||||
@@ -634,24 +692,24 @@ msgstr "Elrendezés"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
msgstr ""
|
||||
msgstr "Terhelési átlag"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
msgstr "Terhelési átlag 15p"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 1m"
|
||||
msgstr ""
|
||||
msgstr "Terhelési átlag 1p"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
msgstr "Terhelési átlag 5p"
|
||||
|
||||
#. Short label for load average
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Load Avg"
|
||||
msgstr ""
|
||||
msgstr "Terhelési átlag"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Bejelentkezés sikertelen"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Naplók"
|
||||
@@ -682,13 +741,14 @@ msgstr "A megjelenítési és értesítési beállítások kezelése."
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Manual setup instructions"
|
||||
msgstr ""
|
||||
msgstr "Manuális beállítási lépések"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Max 1 min"
|
||||
msgstr "Maximum 1 perc"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "RAM"
|
||||
@@ -702,11 +762,17 @@ msgstr "Memóriahasználat"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Docker konténerek memória használata"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Modell"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Név"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Hálózat"
|
||||
@@ -725,15 +791,23 @@ msgstr "Nyilvános interfészek hálózati forgalma"
|
||||
#. Context: Bytes or bits
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Network unit"
|
||||
msgstr ""
|
||||
msgstr "Sávszélesség mértékegysége"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "No results found."
|
||||
msgstr "Nincs találat."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr ""
|
||||
msgstr "Nincs találat."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Ehhez az eszközhöz nem állnak rendelkezésre S.M.A.R.T. attribútumok."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
@@ -768,10 +842,15 @@ msgstr "Menü megnyitása"
|
||||
msgid "Or continue with"
|
||||
msgstr "Vagy folytasd ezzel"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Egyéb"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Felülírja a meglévő riasztásokat"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Oldal"
|
||||
@@ -780,7 +859,7 @@ msgstr "Oldal"
|
||||
#. placeholder {1}: table.getPageCount()
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Page {0} of {1}"
|
||||
msgstr ""
|
||||
msgstr "{0}/{1} oldal"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Pages / Settings"
|
||||
@@ -797,7 +876,7 @@ msgstr "A jelszónak legalább 8 karakternek kell lennie."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be less than 72 bytes."
|
||||
msgstr ""
|
||||
msgstr "A jelszó legfeljebb 72 byte lehet."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Password reset request received"
|
||||
@@ -813,7 +892,16 @@ msgstr "Szüneteltetve"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Szüneteltetve ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Átlagos kihasználtság magonként"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Az idő százalékos aránya minden állapotban"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -852,6 +940,11 @@ msgstr "Kérjük, jelentkezzen be a fiókjába"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Bekapcsolás"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Olvasás"
|
||||
msgid "Received"
|
||||
msgstr "Fogadott"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Frissítés"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Egyszeri jelszó kérése"
|
||||
@@ -892,7 +990,7 @@ msgstr "Jelszó visszaállítása"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Resolved"
|
||||
msgstr ""
|
||||
msgstr "Megoldva"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Resume"
|
||||
@@ -900,11 +998,19 @@ msgstr "Folytatás"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr ""
|
||||
msgstr "Tokenváltás"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Rows per page"
|
||||
msgstr ""
|
||||
msgstr "Sorok száma oldalanként"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "S.M.A.R.T. Részletek"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. Önteszt"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
@@ -917,7 +1023,7 @@ msgstr "Beállítások mentése"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Save system"
|
||||
msgstr ""
|
||||
msgstr "Rendszer mentése"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Search"
|
||||
@@ -935,6 +1041,10 @@ msgstr "Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogy
|
||||
msgid "Sent"
|
||||
msgstr "Elküldve"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Sorozatszám"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Százalékos küszöbértékek beállítása a mérőszínekhez."
|
||||
@@ -965,8 +1075,10 @@ msgstr "Rendezés"
|
||||
#. Context: alert state (active or resolved)
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
msgstr "Állapot"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Swap használat"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -989,7 +1102,7 @@ msgstr "Rendszer"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "System load averages over time"
|
||||
msgstr ""
|
||||
msgstr "Rendszer terhelési átlaga"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
@@ -1004,9 +1117,10 @@ msgid "Table"
|
||||
msgstr "Tábla"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
msgstr "Hőmérséklet"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1015,7 +1129,7 @@ msgstr "Hőmérséklet"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Temperature unit"
|
||||
msgstr ""
|
||||
msgstr "Hőmérséklet mértékegysége"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Temperatures of system sensors"
|
||||
@@ -1039,7 +1153,7 @@ msgstr "Ezt a műveletet nem lehet visszavonni! Véglegesen törli a {name} öss
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "This will permanently delete all selected records from the database."
|
||||
msgstr ""
|
||||
msgstr "Ez véglegesen törli az összes kijelölt bejegyzést az adatbázisból."
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Throughput of {extraFsName}"
|
||||
@@ -1069,13 +1183,13 @@ msgstr "Téma váltása"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr ""
|
||||
msgstr "Tokenek & Ujjlenyomatok"
|
||||
|
||||
#: 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."
|
||||
@@ -1095,15 +1209,15 @@ msgstr "Összes elküldött adat minden interfészenként"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Riaszt, ha az 1 perces terhelési átlag túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Riaszt, ha a 15 perces terhelési átlag túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
msgstr "Riaszt, ha az 5 perces terhelési átlag túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
@@ -1129,15 +1243,19 @@ 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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Típus"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
msgstr ""
|
||||
msgstr "Mértékegység beállítások"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr ""
|
||||
msgstr "Univerzális token"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -1148,11 +1266,15 @@ msgstr "Ismeretlen"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr ""
|
||||
msgstr "Online"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr ""
|
||||
msgstr "Online ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Frissítve"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Üzemidő"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Használat"
|
||||
|
||||
@@ -1185,19 +1308,20 @@ msgstr "Felhasználók"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
msgstr "Érték"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "View"
|
||||
msgstr "Nézet"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Továbbiak megjelenítése"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "View your 200 most recent alerts."
|
||||
msgstr ""
|
||||
msgstr "Legfrissebb 200 riasztásod áttekintése."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Visible Fields"
|
||||
|
||||
1353
internal/site/src/locales/id/id.po
Normal file
1353
internal/site/src/locales/id/id.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,15 @@ msgstr ""
|
||||
"Language: is\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-09-22 23:10\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Icelandic\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: is\n"
|
||||
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 16\n"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -89,7 +89,7 @@ msgstr "Aðgerðir"
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Virkar tilkynningar"
|
||||
|
||||
@@ -116,7 +116,7 @@ msgstr ""
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
@@ -133,7 +133,15 @@ msgstr ""
|
||||
msgid "Alerts"
|
||||
msgstr "Tilkynningar"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Allar gámar"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -205,7 +213,7 @@ msgstr "Beszel notar <0>Shoutrrr</0> til að tengjast vinsælum tilkynningaþjó
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Binary"
|
||||
msgstr "Binary"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -226,6 +234,10 @@ msgstr "Skyndiminni / Biðminni"
|
||||
msgid "Cancel"
|
||||
msgstr "Hætta við"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Aðvörun - möguleiki á gagnatapi"
|
||||
@@ -267,6 +279,14 @@ 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/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Smelltu á gám til að sjá frekari upplýsingar."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr ""
|
||||
@@ -289,7 +309,7 @@ msgstr "Stilltu hvernig þú vilt fá tilkynningar."
|
||||
msgid "Confirm password"
|
||||
msgstr "Staðfestu lykilorð"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr ""
|
||||
|
||||
@@ -348,6 +368,7 @@ msgstr ""
|
||||
msgid "Copy YAML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "Örgjörvi"
|
||||
@@ -384,8 +405,12 @@ msgstr ""
|
||||
msgid "Current state"
|
||||
msgstr "Núverandi ástand"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Yfirlitssíða"
|
||||
|
||||
@@ -402,6 +427,14 @@ msgstr "Eyða"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Nánar"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -508,7 +541,7 @@ msgstr "Villa"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Fór yfir {0}{1} á síðustu {2, plural, one {# mínútu} other {# mínútum}}"
|
||||
|
||||
@@ -532,6 +565,10 @@ msgstr ""
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Villa í auðkenningu"
|
||||
@@ -549,8 +586,10 @@ msgstr "Villa í sendingu prufu skilaboða"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Mistókst að uppfæra tilkynningu"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Sía..."
|
||||
@@ -559,6 +598,10 @@ msgstr "Sía..."
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr ""
|
||||
@@ -576,7 +619,7 @@ msgstr ""
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Full"
|
||||
msgstr "Full"
|
||||
msgstr ""
|
||||
|
||||
#. Context: General settings
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -596,6 +639,10 @@ msgstr "Skjákorts rafmagnsnotkun"
|
||||
msgid "Grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Heilsa"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -604,7 +651,7 @@ msgstr "Homebrew skipun"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
msgstr "Host / IP"
|
||||
msgstr ""
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -615,6 +662,11 @@ msgstr "Aðgerðalaus"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Mynd"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ógilt netfang."
|
||||
@@ -667,6 +719,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Innskránings tilraun misheppnaðist"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Loggar"
|
||||
@@ -689,6 +742,7 @@ msgstr ""
|
||||
msgid "Max 1 min"
|
||||
msgstr "Mest 1 mínúta"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Minni"
|
||||
@@ -702,14 +756,20 @@ msgstr "Minnisnotkun"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Minnisnotkun docker kerfa"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nafn"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
@@ -718,7 +778,6 @@ msgstr "Net traffík docker kerfa"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Network traffic of public interfaces"
|
||||
msgstr ""
|
||||
|
||||
@@ -731,10 +790,18 @@ msgstr ""
|
||||
msgid "No results found."
|
||||
msgstr "Engar niðurstöður fundust."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -772,6 +839,7 @@ msgstr "Eða halda áfram með"
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Yfirskrifa núverandi tilkynningu"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Síða"
|
||||
@@ -850,7 +918,7 @@ msgstr "Vinsamlegast skráðu þig inn á aðganginn þinn"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -906,6 +974,14 @@ msgstr ""
|
||||
msgid "Rows per page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr ""
|
||||
@@ -933,7 +1009,7 @@ msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Sent"
|
||||
msgstr "Sent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
@@ -967,6 +1043,8 @@ msgstr "Raða eftir"
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1059,7 @@ msgid "Swap Usage"
|
||||
msgstr "Skipti minni"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1083,7 @@ msgid "Table"
|
||||
msgstr "Tafla"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
@@ -1129,6 +1209,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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1250,3 +1334,4 @@ msgstr ""
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Notenda stillingar vistaðar."
|
||||
|
||||
|
||||
@@ -8,30 +8,15 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-08-28 23:21\n"
|
||||
"PO-Revision-Date: 2025-10-30 21:53\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: beszel\n"
|
||||
"X-Crowdin-Project-ID: 733311\n"
|
||||
"X-Crowdin-Language: it\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 "{0, plural, one {# giorno} other {# giorni}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 3600)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{0, plural, one {# ora} other {# ore}}"
|
||||
|
||||
#. placeholder {0}: Math.trunc(system.info.u / 60)
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
|
||||
msgstr "{0, plural, one {# minuto} other {# minuti}}"
|
||||
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
|
||||
"X-Crowdin-File-ID: 32\n"
|
||||
|
||||
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
|
||||
#. placeholder {1}: table.getFilteredRowModel().rows.length
|
||||
@@ -39,6 +24,18 @@ msgstr "{0, plural, one {# minuto} other {# minuti}}"
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} di {1} righe selezionate."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} giorno} other {{countString} giorni}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
msgstr "{count, plural, one {{countString} ora} other {{countString} ore}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minuto} other {{countString} minuti}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 ora"
|
||||
@@ -89,7 +86,7 @@ msgstr "Azioni"
|
||||
msgid "Active"
|
||||
msgstr "Attivo"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Avvisi Attivi"
|
||||
|
||||
@@ -133,7 +130,15 @@ msgstr "Cronologia Avvisi"
|
||||
msgid "Alerts"
|
||||
msgstr "Avvisi"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Tutti i contenitori"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
@@ -226,6 +231,10 @@ msgstr "Cache / Buffer"
|
||||
msgid "Cancel"
|
||||
msgstr "Annulla"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
msgstr "Capacità"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Attenzione - possibile perdita di dati"
|
||||
@@ -267,6 +276,14 @@ msgstr "Controlla i log per maggiori dettagli."
|
||||
msgid "Check your notification service"
|
||||
msgstr "Controlla il tuo servizio di notifica"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Fare clic su un contenitore per visualizzare ulteriori informazioni."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Fare clic su un dispositivo per visualizzare più informazioni."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Clicca su un sistema per visualizzare più informazioni."
|
||||
@@ -289,7 +306,7 @@ msgstr "Configura come ricevere le notifiche di avviso."
|
||||
msgid "Confirm password"
|
||||
msgstr "Conferma password"
|
||||
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Connection is down"
|
||||
msgstr "La connessione è interrotta"
|
||||
|
||||
@@ -348,12 +365,22 @@ msgstr "Copia il contenuto<0>docker-compose.yml</0> per l'agente qui sotto, o re
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copia YAML"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
msgstr "Core CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
msgstr "Suddivisione tempo CPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "CPU Usage"
|
||||
msgstr "Utilizzo CPU"
|
||||
@@ -384,8 +411,12 @@ msgstr "Upload cumulativo"
|
||||
msgid "Current state"
|
||||
msgstr "Stato attuale"
|
||||
|
||||
#. Power Cycles
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Cycles"
|
||||
msgstr "Cicli"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/home.tsx
|
||||
msgid "Dashboard"
|
||||
msgstr "Cruscotto"
|
||||
|
||||
@@ -402,6 +433,14 @@ msgstr "Elimina"
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "Elimina impronta digitale"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Dettagli"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
msgstr "Dispositivo"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Discharging"
|
||||
@@ -508,7 +547,7 @@ msgstr "Errore"
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/routes/home.tsx
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Supera {0}{1} negli ultimi {2, plural, one {# minuto} other {# minuti}}"
|
||||
|
||||
@@ -532,6 +571,10 @@ msgstr "Esporta la configurazione attuale dei tuoi sistemi."
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Failed Attributes:"
|
||||
msgstr "Attributi falliti:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Autenticazione fallita"
|
||||
@@ -549,8 +592,10 @@ msgstr "Invio della notifica di test fallito"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Aggiornamento dell'avviso fallito"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filtra..."
|
||||
@@ -559,6 +604,10 @@ msgstr "Filtra..."
|
||||
msgid "Fingerprint"
|
||||
msgstr "Impronta digitale"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Per <0>{min}</0> {min, plural, one {minuto} other {minuti}}"
|
||||
@@ -596,6 +645,10 @@ msgstr "Consumo della GPU"
|
||||
msgid "Grid"
|
||||
msgstr "Griglia"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Stato"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
@@ -615,6 +668,11 @@ msgstr "Inattiva"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "Se hai perso la password del tuo account amministratore, puoi reimpostarla utilizzando il seguente comando."
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Immagine"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Indirizzo email non valido."
|
||||
@@ -667,6 +725,7 @@ msgid "Login attempt failed"
|
||||
msgstr "Tentativo di accesso fallito"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Log"
|
||||
@@ -689,6 +748,7 @@ msgstr "Istruzioni di configurazione manuale"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Memory"
|
||||
msgstr "Memoria"
|
||||
@@ -702,11 +762,17 @@ msgstr "Utilizzo Memoria"
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Utilizzo della memoria dei container Docker"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Modello"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nome"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Rete"
|
||||
@@ -731,10 +797,18 @@ msgstr "Unità rete"
|
||||
msgid "No results found."
|
||||
msgstr "Nessun risultato trovato."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No results."
|
||||
msgstr "Nessun risultato."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "Nessun attributo S.M.A.R.T. disponibile per questo dispositivo."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -768,10 +842,15 @@ msgstr "Apri menu"
|
||||
msgid "Or continue with"
|
||||
msgstr "Oppure continua con"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
msgstr "Altro"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Sovrascrivi avvisi esistenti"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Page"
|
||||
msgstr "Pagina"
|
||||
@@ -815,6 +894,15 @@ msgstr "In pausa"
|
||||
msgid "Paused ({pausedSystemsLength})"
|
||||
msgstr "In pausa ({pausedSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Per-core average utilization"
|
||||
msgstr "Utilizzo medio per core"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Percentuale di tempo trascorso in ogni stato"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Si prega di <0>configurare un server SMTP</0> per garantire la consegna degli avvisi."
|
||||
@@ -852,6 +940,11 @@ msgstr "Si prega di accedere al proprio account"
|
||||
msgid "Port"
|
||||
msgstr "Porta"
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Power On"
|
||||
msgstr "Accensione"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Precise utilization at the recorded time"
|
||||
@@ -876,6 +969,11 @@ msgstr "Lettura"
|
||||
msgid "Received"
|
||||
msgstr "Ricevuto"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Aggiorna"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Request a one-time password"
|
||||
msgstr "Richiedi una password monouso"
|
||||
@@ -906,6 +1004,14 @@ msgstr "Ruota token"
|
||||
msgid "Rows per page"
|
||||
msgstr "Righe per pagina"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Details"
|
||||
msgstr "Dettagli S.M.A.R.T."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "Autotest S.M.A.R.T."
|
||||
|
||||
#: 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."
|
||||
@@ -935,6 +1041,10 @@ msgstr "Vedi <0>impostazioni di notifica</0> per configurare come ricevere gli a
|
||||
msgid "Sent"
|
||||
msgstr "Inviato"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Serial Number"
|
||||
msgstr "Numero di serie"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
msgstr "Imposta le soglie percentuali per i colori dei contatori."
|
||||
@@ -967,6 +1077,8 @@ msgstr "Ordina per"
|
||||
msgid "State"
|
||||
msgstr "Stato"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
@@ -981,6 +1093,7 @@ msgid "Swap Usage"
|
||||
msgstr "Utilizzo Swap"
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1004,6 +1117,7 @@ msgid "Table"
|
||||
msgstr "Tabella"
|
||||
|
||||
#. Temperature label in systems table
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr "Temperatura"
|
||||
@@ -1129,6 +1243,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/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr "Tipo"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Unit preferences"
|
||||
@@ -1154,6 +1272,10 @@ msgstr "Attivo"
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "Attivo ({upSystemsLength})"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Updated"
|
||||
msgstr "Aggiornato"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Carica"
|
||||
@@ -1166,6 +1288,7 @@ msgstr "Tempo di attività"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Usage"
|
||||
msgstr "Utilizzo"
|
||||
|
||||
@@ -1191,6 +1314,7 @@ msgstr "Valore"
|
||||
msgid "View"
|
||||
msgstr "Vista"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "View more"
|
||||
msgstr "Visualizza altro"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user