Compare commits

..

88 Commits

Author SHA1 Message Date
henrygd
02d594cc82 release 0.15.4 2025-11-04 17:23:42 -05:00
henrygd
7d0b5c1c67 update language files 2025-11-04 17:18:57 -05:00
Thiago Alves Cavalcante
d3dc8a7af0 new Portuguese translations 2025-11-04 17:18:07 -05:00
henrygd
d67fefe7c5 new spanish translations by dtornerte 2025-11-04 17:17:02 -05:00
henrygd
4d364c5e4d update language files 2025-11-04 17:06:51 -05:00
henrygd
954400ea45 fix intel_gpu_top parsing when engine instance id is in column (#1230) 2025-11-04 16:02:20 -05:00
henrygd
04b6067e64 add a total line to the tooltip of charts with multiple values #1280
Co-authored-by: Titouan V <titouan.verhille@gmail.com>
2025-11-04 15:41:24 -05:00
henrygd
d77ee5554f add fallback paths for smartctl lookup (#1362, #1363) 2025-11-04 14:06:28 -05:00
henrygd
2e034bdead refactor containers table to fix clock issue causing no results (#1337) 2025-11-04 13:18:34 -05:00
henrygd
fc0947aa04 fix windows extra disk backslash issue (#1361) 2025-11-03 17:42:08 -05:00
henrygd
1d546a4091 update nvidia dockerfile to build latest smartmontools (#1335) 2025-11-02 17:13:47 -05:00
henrygd
f60b3bbbfb release 0.15.3 2025-11-01 16:04:02 -04:00
henrygd
8e99b9f1ad update shoutrrr and gopsutil deps 2025-11-01 14:31:41 -04:00
henrygd
fa5ed2bc11 update translations 2025-11-01 14:09:10 -04:00
henrygd
21d961ab97 sync language files 2025-11-01 13:50:53 -04:00
henrygd
aaa93b84d2 add hebrew + new cpu charts refactoring 2025-11-01 13:34:30 -04:00
henrygd
6a562ce03b add more cpu metrics (#1356)
- adds monitoring for cpu state time and per-core usage

Co-authored-by: Sven van Ginkel <svenvanginkel@icloud.com>
2025-11-01 12:57:58 -04:00
henrygd
3dbc48727e add INTEL_GPU_DEVICE env var (#1285) 2025-11-01 11:12:05 -04:00
henrygd
85ac2e5e9a update env var name to EXCLUDE_CONTAINERS #1352 2025-10-30 19:30:01 -04:00
Sven van Ginkel
af6bd4e505 [Feature] Add env var to exclude containers from being monitored (#1352) 2025-10-30 19:02:09 -04:00
Gabay
e54c4b3499 New translations en.po (Hebrew) 2025-10-30 16:50:14 -04:00
henrygd
078c88f825 add hebrew machine translations 2025-10-30 16:45:33 -04:00
henrygd
85169b6c5e improve parsing of edge case smart power on times (#1347) 2025-10-30 16:32:06 -04:00
henrygd
d0ff8ee2c0 fix disk i/o values in longer charts (#1355) 2025-10-30 14:17:56 -04:00
henrygd
e898768997 fix battery nil pointer error #1353 2025-10-30 12:52:33 -04:00
henrygd
0f5b504f23 release 0.15.2 2025-10-29 01:18:15 -04:00
henrygd
365d291393 improve smart device detection (#1345)
also fix virtual device filtering
2025-10-29 01:16:58 -04:00
henrygd
3dbab24c0f improve identification of smart drive types (#1345) 2025-10-28 22:37:47 -04:00
henrygd
1f67fb7c8d release 0.15.1 2025-10-28 19:30:36 -04:00
henrygd
219e09fc78 update language files 2025-10-28 18:41:39 -04:00
henrygd
cd9c2bd9ab update logs in smart.go
also change max execution time to 2 sec
2025-10-28 17:34:49 -04:00
henrygd
9f969d843c update changelog 2025-10-28 16:52:55 -04:00
henrygd
b22a6472fc missed staging this earlier :) 2025-10-28 16:44:34 -04:00
henrygd
d231ace28e fix SHARE_ALL_SYSTEMS not working for Containers
#1334
2025-10-28 16:25:29 -04:00
henrygd
473cb7f437 merge SMART_DEVICES with devices returned from smartctl scan 2025-10-28 15:38:47 -04:00
henrygd
783ed9f456 cache smartctl scan results for 10 min w/ force option
also add support for sntrealtek
2025-10-28 14:01:45 -04:00
henrygd
9a9a89ee50 handle when power on smart attribute is a string like 0h+0m+0.000s 2025-10-28 13:44:31 -04:00
henrygd
5122d0341d fix S.M.A.R.T. wrong disk is renderer in the DiskSheet table #1336 2025-10-28 12:55:38 -04:00
zjkal
81731689da A small translation error has been fixed (#1343) 2025-10-28 11:09:10 -04:00
henrygd
b3e9857448 add SMART_DEVICES env var (#373, #1335)
also iterate through parsers to try to find a match if type is not defined.
2025-10-27 15:26:29 -04:00
henrygd
2eda9eb0e3 add support for scsi and sntasmedia smart data (#373, #1335) 2025-10-27 14:39:12 -04:00
henrygd
82a5df5048 add secondsToString function 2025-10-27 14:14:17 -04:00
Sven van Ginkel
f11564a7ac Skip virtual disks (#1332) 2025-10-27 11:44:21 -04:00
Sven van Ginkel
9df4d29236 Add sorting to the smart table (#1333) 2025-10-27 11:43:23 -04:00
henrygd
1452817423 update readme 2025-10-26 14:09:14 -04:00
AuthorShin
c57e496f5e Added Container to Supported metrics list on readme.md (#1323) 2025-10-26 14:04:42 -04:00
henrygd
6287f7003c fix text contrast when container details disabled (#1324) 2025-10-26 11:41:21 -04:00
henrygd
37037b1f4e update changelog 2025-10-26 11:34:13 -04:00
henrygd
7cf123a99e fix: limit frame and total sizes when reading docker logs (#1322)
- Add per-frame size limit (1MB) to prevent single massive log entries
- Add total log size limit (5MB) for network transfer and browser rendering
- Gracefully truncate logs that exceed limits instead of consuming unbounded memory
2025-10-26 11:02:18 -04:00
henrygd
97394e775f release 0.15.0 2025-10-26 10:47:55 -04:00
henrygd
d5c381188b update go deps 2025-10-26 10:46:30 -04:00
henrygd
b107d12a62 smart support over ssh + change response code for smart failure 2025-10-26 10:33:34 -04:00
henrygd
e646f2c1fc fix inactive tab losing container table data 2025-10-26 10:32:49 -04:00
henrygd
b18528d24a update translations 2025-10-25 18:26:46 -04:00
henrygd
a6e64df399 update language files 2025-10-25 17:20:10 -04:00
Klaus Dandl
66ba21dd41 New German translations 2025-10-25 17:19:14 -04:00
Thor B.
1851e7a111 New Danish translations 2025-10-25 17:00:15 -04:00
henrygd
74b78e96b3 pre release refactoring + update changelog 2025-10-25 16:34:32 -04:00
henrygd
a9657f9c00 add CONTAINER_DETAILS env var (#1305) 2025-10-25 15:33:01 -04:00
henrygd
1dee63a0eb update HasReadableBattery to check all batteries 2025-10-25 14:06:25 -04:00
Nathan Heaps
d608cf0955 fix: send battery stats even if some batteries are missing stats (#1287)
Implement battery error reporting mechanism

Added error reporting for battery information retrieval.

Handles cases like bluetooth devices where `battery` finds it but it has incomplete information about that battery, which would have otherwise caused a null pointer error.

Related: #1254
2025-10-25 13:23:29 -04:00
henrygd
b9139a1f9b strip env vars from container detail (#1305) 2025-10-25 12:58:13 -04:00
henrygd
7f372c46db add alpine image + add smartmontools to intel / nvidia images 2025-10-25 11:59:57 -04:00
henrygd
40010ad9b9 small frontend refactoring / style updates 2025-10-25 11:58:28 -04:00
henrygd
5927f45a4a install-agent.sh: add 'beszel' user to disk group for device access 2025-10-25 11:56:26 -04:00
henrygd
962613df7c Add initial S.M.A.R.T. support
- Implement SmartManager for collecting SMART data from SATA and NVMe drives
- Add smartctl-based data collection with standby mode detection
- Support comprehensive SMART attributes parsing and storage
- Add hub API endpoint for fetching SMART data from agents
- Create SMART table UI with detailed disk information

Co-authored-by: geekifan <i@ifan.dev>
2025-10-24 18:54:56 -04:00
Sven van Ginkel
92b1f236e3 [Feature] Let y axis for temperature not start at 0 (#1307)
* Let y axis not start at 0

* use auto auto
2025-10-22 11:17:42 -04:00
henrygd
a911670a2d release 0.14.1 2025-10-20 17:44:31 -04:00
henrygd
b0cb0c2269 update language files 2025-10-20 17:31:07 -04:00
Teo
735d03577f New Croatian translations 2025-10-20 17:28:57 -04:00
Bruno
a33f88d822 New translations en.po (French) 2025-10-20 17:22:53 -04:00
henrygd
dfd1fc8fda add image name to containers table (#1302) 2025-10-20 17:11:26 -04:00
henrygd
1df08801a2 fix unfound filter values hiding containers chart (#1301) 2025-10-20 14:33:49 -04:00
henrygd
62f5f986bb add spacing for long temperature chart tooltip (#1299) 2025-10-20 14:32:27 -04:00
henrygd
a87b9af9d5 Add MFA_OTP env var to enable email-based one-time password for users and/or superusers 2025-10-20 14:29:56 -04:00
henrygd
03900e54cc correctly sort status column in containers table (#1294) 2025-10-19 11:18:32 -04:00
henrygd
f4abbd1a5b fix system link in container sheet when serving on subpath 2025-10-18 20:02:58 -04:00
henrygd
77ed90cb4a release 0.14.0 :) 2025-10-18 19:36:23 -04:00
henrygd
2fe3b1adb1 increase chart tooltip z-index 2025-10-18 19:20:16 -04:00
henrygd
f56093d0f0 hide container table if no containers 2025-10-18 18:52:59 -04:00
henrygd
77dba42f17 update language files 2025-10-18 18:51:09 -04:00
henrygd
e233a0b0dc new zh translations 2025-10-18 17:33:26 -04:00
derilevi
18e4c88875 new Hungarian translations 2025-10-18 17:32:34 -04:00
Rasko
904a6038cd new Norwegian translations 2025-10-18 17:30:24 -04:00
henrygd
ae55b86493 improve chart filtering logic to support multiple terms (#1274) 2025-10-18 17:10:08 -04:00
henrygd
5360f762e4 expand container monitoring functionality (#928)
- Add new /containers route with virtualized table showing all containers across systems
- Implement container stats collection (CPU, memory, network usage) with health status tracking
- Add container logs and info API endpoints with syntax highlighting using Shiki
- Create detailed container views with fullscreen logs/info dialogs and refresh functionality
- Add container table to individual system pages with lazy loading
- Implement container record storage with automatic cleanup and historical averaging
- Update navbar with container navigation icon
- Extract reusable ActiveAlerts component from home page
- Add FooterRepoLink component for consistent GitHub/version display
- Enhance filtering and search capabilities across container tables
2025-10-18 16:32:16 -04:00
henrygd
0d464787f2 logo color on hover 2025-10-18 15:48:59 -04:00
henrygd
24f72ef596 update collections snapshot 2025-10-18 15:47:12 -04:00
112 changed files with 18183 additions and 4721 deletions

View File

@@ -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] != '' }}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
@@ -271,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:

View File

@@ -4,10 +4,12 @@ import (
"math"
"runtime"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/cpu"
)
var lastCpuTimes = make(map[uint16]cpu.TimesStat)
var lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)
// init initializes the CPU monitoring by storing the initial CPU times
// for the default 60-second cache interval.
@@ -15,23 +17,92 @@ 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
}
}
// 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
}
// calculateBusy calculates the CPU busy percentage between two time points.
@@ -41,13 +112,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.

View File

@@ -31,6 +31,7 @@ func (a *Agent) initializeDiskInfo() {
filesystem, _ := GetEnv("FILESYSTEM")
efPath := "/extra-filesystems"
hasRoot := false
isWindows := runtime.GOOS == "windows"
partitions, err := disk.Partitions(false)
if err != nil {
@@ -38,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"},
// )
@@ -52,7 +60,7 @@ func (a *Agent) initializeDiskInfo() {
// Helper function to add a filesystem to fsStats if it doesn't exist
addFsStat := func(device, mountpoint string, root bool, customName ...string) {
var key string
if runtime.GOOS == "windows" {
if isWindows {
key = device
} else {
key = filepath.Base(device)

View File

@@ -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,18 +29,16 @@ const (
dockerTimeoutMs = 2100
// Maximum realistic network speed (5 GB/s) to detect bad deltas
maxNetworkSpeedBps uint64 = 5e9
// Container and health constants
composeProjectLabel = "com.docker.compose.project"
healthStatusNone = "none"
containerStateRunning = "running"
containerStateUnknown = "unknown"
volumeTypeVolume = "volume"
diskOpRead = "read"
diskOpReadCap = "Read"
diskOpWrite = "write"
diskOpWriteCap = "Write"
// 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 {
@@ -52,8 +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
volumeSizeCache map[string]float64 // Cached volume sizes (name -> size in MB)
volumeSizeUpdated time.Time // Last time volume sizes were updated
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
@@ -65,11 +66,6 @@ type dockerManager struct {
// cacheTimeMs -> DeltaTracker for network bytes sent/received
networkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
networkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
// Disk I/O delta trackers - one per cache time to avoid interference
// cacheTimeMs -> DeltaTracker for disk bytes read/written
diskReadTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
diskWriteTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
}
// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests
@@ -100,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")
@@ -127,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
@@ -176,9 +192,8 @@ func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats,
}
}
// prepare network and disk trackers for next interval for this cache time
// prepare network trackers for next interval for this cache time
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
dm.cycleDiskDeltasForCacheTime(cacheTimeMs)
return stats, nil
}
@@ -257,32 +272,6 @@ func (dm *dockerManager) cycleNetworkDeltasForCacheTime(cacheTimeMs uint16) {
}
}
// getDiskTracker returns the DeltaTracker for disk I/O for a specific cache time, creating it if needed
func (dm *dockerManager) getDiskTracker(cacheTimeMs uint16, isRead bool) *deltatracker.DeltaTracker[string, uint64] {
var trackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
if isRead {
trackers = dm.diskReadTrackers
} else {
trackers = dm.diskWriteTrackers
}
if trackers[cacheTimeMs] == nil {
trackers[cacheTimeMs] = deltatracker.NewDeltaTracker[string, uint64]()
}
return trackers[cacheTimeMs]
}
// cycleDiskDeltasForCacheTime cycles the disk delta trackers for a specific cache time
func (dm *dockerManager) cycleDiskDeltasForCacheTime(cacheTimeMs uint16) {
if dm.diskReadTrackers[cacheTimeMs] != nil {
dm.diskReadTrackers[cacheTimeMs].Cycle()
}
if dm.diskWriteTrackers[cacheTimeMs] != nil {
dm.diskWriteTrackers[cacheTimeMs].Cycle()
}
}
// calculateNetworkStats calculates network sent/receive deltas using DeltaTracker
func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats *container.ApiStats, stats *container.Stats, initialized bool, name string, cacheTimeMs uint16) (uint64, uint64) {
var total_sent, total_recv uint64
@@ -328,50 +317,6 @@ func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats
return sent_delta, recv_delta
}
// calculateDiskStats calculates disk read/write deltas using DeltaTracker
func (dm *dockerManager) calculateDiskStats(ctr *container.ApiInfo, apiStats *container.ApiStats, stats *container.Stats, initialized bool, cacheTimeMs uint16) (uint64, uint64) {
var total_read, total_write uint64
for _, entry := range apiStats.BlkioStats.IoServiceBytesRecursive {
switch entry.Op {
case diskOpRead, diskOpReadCap:
total_read += entry.Value
case diskOpWrite, diskOpWriteCap:
total_write += entry.Value
}
}
// Get the DeltaTracker for this specific cache time
readTracker := dm.getDiskTracker(cacheTimeMs, true)
writeTracker := dm.getDiskTracker(cacheTimeMs, false)
// Set current values in the cache-time-specific DeltaTracker
readTracker.Set(ctr.IdShort, total_read)
writeTracker.Set(ctr.IdShort, total_write)
// Get deltas (bytes since last measurement)
read_delta_raw := readTracker.Delta(ctr.IdShort)
write_delta_raw := writeTracker.Delta(ctr.IdShort)
// Calculate bytes per second if we have previous data
var read_delta, write_delta uint64
if initialized {
millisecondsElapsed := uint64(time.Since(stats.PrevReadTime).Milliseconds())
if millisecondsElapsed > 0 {
if read_delta_raw > 0 {
read_delta = read_delta_raw * 1000 / millisecondsElapsed
}
if write_delta_raw > 0 {
write_delta = write_delta_raw * 1000 / millisecondsElapsed
}
}
}
// Store current disk values for legacy compatibility
stats.PrevDisk.Read, stats.PrevDisk.Write = total_read, total_write
return read_delta, write_delta
}
// validateCpuPercentage checks if CPU percentage is within valid range
func validateCpuPercentage(cpuPct float64, containerName string) error {
if cpuPct > 100 {
@@ -381,21 +326,54 @@ func validateCpuPercentage(cpuPct float64, containerName string) error {
}
// updateContainerStatsValues updates the final stats values
func updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemory uint64, sent_delta, recv_delta, read_delta, write_delta uint64, readTime time.Time) {
func updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemory uint64, sent_delta, recv_delta uint64, readTime time.Time) {
stats.Cpu = twoDecimals(cpuPct)
stats.Mem = bytesToMegabytes(float64(usedMemory))
stats.NetworkSent = bytesToMegabytes(float64(sent_delta))
stats.NetworkRecv = bytesToMegabytes(float64(recv_delta))
stats.DiskRead = bytesToMegabytes(float64(read_delta))
stats.DiskWrite = bytesToMegabytes(float64(write_delta))
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
}
@@ -406,68 +384,21 @@ 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
}
// Update name in case it changed
stats.Name = name
stats.Id = ctr.IdShort
// Set container metadata
stats.IdShort = ctr.IdShort
stats.Status = ctr.State
if stats.Status == "" {
stats.Status = containerStateUnknown
}
// Set health status
stats.Health = healthStatusNone
if ctr.Health != "" {
stats.Health = ctr.Health
}
// Set Docker Compose project name
if ctr.Labels != nil {
if projectName, exists := ctr.Labels[composeProjectLabel]; exists {
stats.Project = projectName
}
}
// Calculate uptime for running containers
if ctr.StartedAt > 0 && stats.Status == containerStateRunning {
startedTime := time.Unix(ctr.StartedAt, 0)
stats.Uptime = twoDecimals(time.Since(startedTime).Seconds())
} else {
stats.Uptime = 0
}
// Collect volume information and fetch sizes
volumeCount := 0
for _, mount := range ctr.Mounts {
if mount.Type == volumeTypeVolume && mount.Name != "" {
volumeCount++
}
}
if volumeCount > 0 {
stats.Volumes = make(map[string]float64, volumeCount)
for _, mount := range ctr.Mounts {
if mount.Type == volumeTypeVolume && mount.Name != "" {
// Fetch volume size using Docker system df API
size := dm.getVolumeSize(mount.Name)
stats.Volumes[mount.Name] = size
}
}
} else {
stats.Volumes = nil
}
statusText, health := parseDockerStatus(ctr.Status)
stats.Status = statusText
stats.Health = health
// reset current stats
stats.Cpu = 0
stats.Mem = 0
stats.NetworkSent = 0
stats.NetworkRecv = 0
stats.DiskRead = 0
stats.DiskWrite = 0
res := dm.apiStats
res.Networks = nil
@@ -517,11 +448,8 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeM
}
stats.PrevNet.Sent, stats.PrevNet.Recv = total_sent, total_recv
// Calculate disk I/O stats using DeltaTracker
read_delta, write_delta := dm.calculateDiskStats(ctr, res, stats, initialized, cacheTimeMs)
// Update final stats values
updateContainerStatsValues(stats, cpuPct, usedMemory, sent_delta, recv_delta, read_delta, write_delta, res.Read)
updateContainerStatsValues(stats, cpuPct, usedMemory, sent_delta, recv_delta, res.Read)
// store per-cache-time read time for Windows CPU percent calc
dm.lastCpuReadTime[cacheTimeMs][ctr.IdShort] = res.Read
@@ -597,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,
@@ -606,7 +547,7 @@ func newDockerManager(a *Agent) *dockerManager {
sem: make(chan struct{}, 5),
apiContainerList: []*container.ApiInfo{},
apiStats: &container.ApiStats{},
volumeSizeCache: make(map[string]float64),
excludeContainers: excludeContainers,
// Initialize cache-time-aware tracking structures
lastCpuContainer: make(map[uint16]map[string]uint64),
@@ -614,8 +555,6 @@ func newDockerManager(a *Agent) *dockerManager {
lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
diskReadTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
diskWriteTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
}
// If using podman, return client
@@ -670,49 +609,6 @@ func (dm *dockerManager) checkDockerVersion() {
}
}
// getVolumeSize returns the cached size of a Docker volume
// Refreshes the cache every 5 minutes using the system df API
// Returns size in MB (megabytes)
func (dm *dockerManager) getVolumeSize(volumeName string) float64 {
// Refresh cache if older than 5 minutes
if time.Since(dm.volumeSizeUpdated) > 5*time.Minute {
dm.refreshVolumeSizes()
}
return dm.volumeSizeCache[volumeName]
}
// refreshVolumeSizes fetches all volume sizes from Docker and updates the cache
func (dm *dockerManager) refreshVolumeSizes() {
type volumeInfo struct {
Name string
UsageData struct {
Size int64
}
}
type systemDfResponse struct {
Volumes []volumeInfo
}
resp, err := dm.client.Get("http://localhost/system/df")
if err != nil {
return
}
var dfData systemDfResponse
if err := dm.decode(resp, &dfData); err != nil {
return
}
// Update all volume sizes in cache
for _, vol := range dfData.Volumes {
// Convert bytes to MB (megabytes)
dm.volumeSizeCache[vol.Name] = float64(vol.UsageData.Size) / 1_000_000
}
dm.volumeSizeUpdated = time.Now()
}
// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe.
func (dm *dockerManager) decode(resp *http.Response, d any) error {
if dm.buf == nil {
@@ -740,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
}

View File

@@ -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)
})
}
}

View File

@@ -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()
@@ -129,7 +134,9 @@ func (gm *GPUManager) parseIntelHeaders(header1 string, header2 string) (engineN
powerIndex = -1 // Initialize to -1, will be set to actual index if found
// Collect engine names from header1
for _, col := range h1 {
key := strings.TrimRightFunc(col, func(r rune) bool { return r >= '0' && r <= '9' })
key := strings.TrimRightFunc(col, func(r rune) bool {
return (r >= '0' && r <= '9') || r == '/'
})
var friendly string
switch key {
case "RCS":

View File

@@ -4,8 +4,10 @@
package agent
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -1437,6 +1439,15 @@ func TestParseIntelHeaders(t *testing.T) {
wantPowerIndex: 4, // "gpu" is at index 4
wantPreEngineCols: 8, // 17 total cols - 3*3 = 8
},
{
name: "basic headers with RCS BCS VCS using index in name",
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS/0 BCS/1 VCS/2",
header2: " req act /s % gpu pkg rd wr % se wa % se wa % se wa",
wantEngineNames: []string{"RCS", "BCS", "VCS"},
wantFriendlyNames: []string{"Render/3D", "Blitter", "Video"},
wantPowerIndex: 4, // "gpu" is at index 4
wantPreEngineCols: 8, // 17 total cols - 3*3 = 8
},
{
name: "headers with only RCS",
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS",
@@ -1624,3 +1635,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")
}

View File

@@ -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)
}

View File

@@ -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)
}

914
agent/smart.go Normal file
View File

@@ -0,0 +1,914 @@
package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"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
binPath string
}
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, sm.binPath, "--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, sm.binPath, 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, sm.binPath, 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(&copyDev, prev)
}
finalDevices = append(finalDevices, &copyDev)
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(&copyDev, prev)
} else if copyDev.Type != "" {
copyDev.parserType = normalizeParserType(copyDev.Type)
}
finalDevices = append(finalDevices, &copyDev)
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() (string, error) {
if path, err := exec.LookPath("smartctl"); err == nil {
return path, nil
}
locations := []string{}
if runtime.GOOS == "windows" {
locations = append(locations,
"C:\\Program Files\\smartmontools\\bin\\smartctl.exe",
)
} else {
locations = append(locations, "/opt/homebrew/bin/smartctl")
}
for _, location := range locations {
if _, err := os.Stat(location); err == nil {
return location, nil
}
}
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),
}
path, err := sm.detectSmartctl()
if err != nil {
slog.Debug(err.Error())
return nil, err
}
slog.Debug("smartctl", "path", path)
sm.binPath = path
return sm, nil
}

590
agent/smart_test.go Normal file
View 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)
})
}
}

View File

@@ -78,16 +78,29 @@ 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)
}
// per-core cpu usage
if perCoreUsage, err := getPerCoreCpuUsage(cacheTimeMs); err == nil {
systemStats.CpuCoresUsage = perCoreUsage
}
// load average

View 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"
}
}
}

View 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"
}
]
}

View 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
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import "github.com/blang/semver"
const (
// Version is the current version of the application.
Version = "0.13.2"
Version = "0.15.4"
// AppName is the name of the application.
AppName = "beszel"
)

39
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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"`
}

View 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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -4,29 +4,25 @@ import "time"
// Docker container info from /containers/json
type ApiInfo struct {
Id string
IdShort string
Names []string
Status string
Health string `json:"Health,omitempty"` // Container health status
Created int64 `json:"Created,omitempty"` // Container creation timestamp
StartedAt int64 `json:"StartedAt,omitempty"` // Container start timestamp
FinishedAt int64 `json:"FinishedAt,omitempty"` // Container finish timestamp
State string `json:"State,omitempty"` // Container state (running, stopped, etc.)
// Image string
Id string
IdShort string
Names []string
Status string
State string
Image string
// ImageID string
// Command string
// Created int64
// Ports []Port
// SizeRw int64 `json:",omitempty"`
// SizeRootFs int64 `json:",omitempty"`
Labels map[string]string
// State string
// Labels map[string]string
// HostConfig struct {
// NetworkMode string `json:",omitempty"`
// Annotations map[string]string `json:",omitempty"`
// }
// NetworkSettings *SummaryNetworkSettings
Mounts []MountPoint
// Mounts []MountPoint
}
// Docker container resources from /containers/{id}/stats
@@ -36,7 +32,6 @@ type ApiStats struct {
Networks map[string]NetworkStats
CPUStats CPUStats `json:"cpu_stats"`
MemoryStats MemoryStats `json:"memory_stats"`
BlkioStats BlkioStats `json:"blkio_stats"`
}
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
@@ -103,58 +98,42 @@ type NetworkStats struct {
TxBytes uint64 `json:"tx_bytes"`
}
type BlkioStats struct {
IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive"`
IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive"`
}
type BlkioStatEntry struct {
Major uint64 `json:"major"`
Minor uint64 `json:"minor"`
Op string `json:"op"`
Value uint64 `json:"value"`
}
type prevNetStats struct {
Sent uint64
Recv uint64
}
type prevDiskStats struct {
Read uint64
Write 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"`
Cpu float64 `json:"c" cbor:"1,keyasint"`
Mem float64 `json:"m" cbor:"2,keyasint"`
NetworkSent float64 `json:"ns" cbor:"3,keyasint"`
NetworkRecv float64 `json:"nr" cbor:"4,keyasint"`
DiskRead float64 `json:"dr" cbor:"5,keyasint"` // Disk read rate in MB/s
DiskWrite float64 `json:"dw" cbor:"6,keyasint"` // Disk write rate in MB/s
Volumes map[string]float64 `json:"v,omitempty" cbor:"7,keyasint"` // Volume name to size mapping
Health string `json:"h,omitempty" cbor:"8,keyasint"` // Container health status
Status string `json:"s,omitempty" cbor:"9,keyasint"` // Container status (running, stopped, etc.)
Uptime float64 `json:"u,omitempty" cbor:"10,keyasint"` // Container uptime in seconds
Project string `json:"p,omitempty" cbor:"11,keyasint"` // Docker Compose project name
IdShort string `json:"idShort,omitempty" cbor:"12,keyasint"` // Container short ID for frontend
CpuSystem uint64 `json:"-"`
CpuContainer uint64 `json:"-"`
PrevNet prevNetStats `json:"-"`
PrevDisk prevDiskStats `json:"-"`
PrevReadTime time.Time `json:"-"`
}
Name string `json:"n" cbor:"0,keyasint"`
Cpu float64 `json:"c" cbor:"1,keyasint"`
Mem float64 `json:"m" cbor:"2,keyasint"`
NetworkSent float64 `json:"ns" cbor:"3,keyasint"`
NetworkRecv float64 `json:"nr" cbor:"4,keyasint"`
// MountPoint represents a mount point in a container
type MountPoint struct {
Type string `json:"Type"`
Name string `json:"Name"`
Source string `json:"Source"`
Destination string `json:"Destination"`
Driver string `json:"Driver,omitempty"`
Mode string `json:"Mode"`
RW bool `json:"RW"`
Propagation string `json:"Propagation"`
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:"-"`
PrevNet prevNetStats `json:"-"`
PrevReadTime time.Time `json:"-"`
}

View 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"`
}

View 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)
}

View File

@@ -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,28 @@ 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..]
}
// 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 {

View File

@@ -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 {

View File

@@ -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
{

View File

@@ -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

View File

@@ -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.

View File

@@ -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)
}
}()
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -414,8 +463,6 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
sums[stat.Name].Mem += stat.Mem
sums[stat.Name].NetworkSent += stat.NetworkSent
sums[stat.Name].NetworkRecv += stat.NetworkRecv
sums[stat.Name].DiskRead += stat.DiskRead
sums[stat.Name].DiskWrite += stat.DiskWrite
}
}
@@ -427,8 +474,6 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
Mem: twoDecimals(value.Mem / count),
NetworkSent: twoDecimals(value.NetworkSent / count),
NetworkRecv: twoDecimals(value.NetworkRecv / count),
DiskRead: twoDecimals(value.DiskRead / count),
DiskWrite: twoDecimals(value.DiskWrite / count),
})
}
return result
@@ -441,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
@@ -510,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.

View File

@@ -11,10 +11,10 @@ export default defineConfig({
"es",
"fa",
"fr",
"he",
"hr",
"hu",
"it",
"is",
"ja",
"ko",
"nl",

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.13.2",
"version": "0.15.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.13.2",
"version": "0.15.4",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -42,11 +42,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",
@@ -332,9 +333,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.3.tgz",
"integrity": "sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.4.tgz",
"integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@@ -348,20 +349,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.2.3",
"@biomejs/cli-darwin-x64": "2.2.3",
"@biomejs/cli-linux-arm64": "2.2.3",
"@biomejs/cli-linux-arm64-musl": "2.2.3",
"@biomejs/cli-linux-x64": "2.2.3",
"@biomejs/cli-linux-x64-musl": "2.2.3",
"@biomejs/cli-win32-arm64": "2.2.3",
"@biomejs/cli-win32-x64": "2.2.3"
"@biomejs/cli-darwin-arm64": "2.2.4",
"@biomejs/cli-darwin-x64": "2.2.4",
"@biomejs/cli-linux-arm64": "2.2.4",
"@biomejs/cli-linux-arm64-musl": "2.2.4",
"@biomejs/cli-linux-x64": "2.2.4",
"@biomejs/cli-linux-x64-musl": "2.2.4",
"@biomejs/cli-win32-arm64": "2.2.4",
"@biomejs/cli-win32-x64": "2.2.4"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.3.tgz",
"integrity": "sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz",
"integrity": "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==",
"cpu": [
"arm64"
],
@@ -376,9 +377,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.3.tgz",
"integrity": "sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz",
"integrity": "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==",
"cpu": [
"x64"
],
@@ -393,9 +394,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.3.tgz",
"integrity": "sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz",
"integrity": "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==",
"cpu": [
"arm64"
],
@@ -410,9 +411,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.3.tgz",
"integrity": "sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz",
"integrity": "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==",
"cpu": [
"arm64"
],
@@ -427,9 +428,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.3.tgz",
"integrity": "sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz",
"integrity": "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==",
"cpu": [
"x64"
],
@@ -444,9 +445,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.3.tgz",
"integrity": "sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz",
"integrity": "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==",
"cpu": [
"x64"
],
@@ -461,9 +462,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.3.tgz",
"integrity": "sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz",
"integrity": "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==",
"cpu": [
"arm64"
],
@@ -478,9 +479,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.3.tgz",
"integrity": "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz",
"integrity": "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==",
"cpu": [
"x64"
],
@@ -2746,6 +2747,73 @@
"win32"
]
},
"node_modules/@shikijs/core": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.13.0.tgz",
"integrity": "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.13.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.13.0.tgz",
"integrity": "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.13.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.3"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.13.0.tgz",
"integrity": "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.13.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.13.0.tgz",
"integrity": "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.13.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.13.0.tgz",
"integrity": "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.13.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz",
"integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -3416,6 +3484,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -3443,6 +3520,15 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
@@ -3473,6 +3559,12 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -3490,6 +3582,12 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.0.1.tgz",
@@ -3773,6 +3871,16 @@
],
"license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3790,6 +3898,26 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -3915,6 +4043,16 @@
"node": ">=0.1.90"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -4139,6 +4277,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -4155,6 +4302,19 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@@ -4393,6 +4553,52 @@
"node": ">=8"
}
},
"node_modules/hast-util-to-html": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -4973,6 +5179,116 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@ungap/structured-clone": "^1.0.0",
"devlop": "^1.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-types": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -5138,6 +5454,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz",
"integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==",
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.1",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"node_modules/ora": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
@@ -5351,6 +5684,16 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/pseudolocale": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pseudolocale/-/pseudolocale-2.1.0.tgz",
@@ -5567,6 +5910,30 @@
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-recursion": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -5698,6 +6065,22 @@
"node": ">=8"
}
},
"node_modules/shiki": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.13.0.tgz",
"integrity": "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "3.13.0",
"@shikijs/engine-javascript": "3.13.0",
"@shikijs/engine-oniguruma": "3.13.0",
"@shikijs/langs": "3.13.0",
"@shikijs/themes": "3.13.0",
"@shikijs/types": "3.13.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -5734,6 +6117,16 @@
"node": ">=0.10.0"
}
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -5808,6 +6201,20 @@
"node": ">=8"
}
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
@@ -5966,6 +6373,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
@@ -6003,6 +6420,74 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -6098,6 +6583,34 @@
}
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
@@ -6121,9 +6634,9 @@
}
},
"node_modules/vite": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6341,6 +6854,16 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"devOptional": true,
"license": "ISC"
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.13.2",
"version": "0.15.4",
"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"
}
}
}

View 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("")])
}

View File

@@ -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>

View File

@@ -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,25 @@ export default function AreaChartDefault({
domain,
legend,
itemSorter,
showTotal = false,
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
showTotal?: boolean
itemSorter?: (a: any, b: any) => number
reverseStackOrder?: boolean
hideYAxis?: boolean
// logRender?: boolean
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
@@ -56,21 +63,29 @@ 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
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
domain={domain ?? [0, max ?? "auto"]}
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
tickLine={false}
axisLine={false}
/>
{!hideYAxis && (
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}
domain={domain ?? [0, max ?? "auto"]}
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
tickLine={false}
axisLine={false}
/>
)}
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
@@ -81,6 +96,7 @@ export default function AreaChartDefault({
<ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={contentFormatter}
showTotal={showTotal}
/>
}
/>
@@ -99,13 +115,14 @@ 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>
)
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled, showTotal])
}

View File

@@ -1,32 +1,20 @@
// import Spinner from '../spinner'
import { useStore } from "@nanostores/react"
import { memo, useMemo } from "react"
import React from "react"
import { Area, AreaChart, CartesianGrid, Line, LineChart, YAxis } from "recharts"
import { Badge } from "@/components/ui/badge"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { Separator } from "@/components/ui/separator"
import { ChartType, Unit } from "@/lib/enums"
import { $containerColors, $containerFilter, $stackFilter, $userSettings } from "@/lib/stores"
import {
chartMargin,
cn,
decimalString,
formatBytes,
formatShortDate,
generateFallbackColor,
getSizeAndUnit,
toFixedFloat,
toFixedWithoutTrailingZeros,
} from "@/lib/utils"
import { $containerFilter, $userSettings } from "@/lib/stores"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
import type { ChartData } from "@/types"
import { Separator } from "../ui/separator"
import { useYAxisWidth } from "./hooks"
export default memo(function ContainerChart({
dataKey,
chartData,
chartType,
chartConfig: propChartConfig,
chartConfig,
unit = "%",
}: {
dataKey: string
@@ -35,168 +23,13 @@ export default memo(function ContainerChart({
chartConfig: ChartConfig
unit?: string
}) {
const containerFilter = useStore($containerFilter)
const stackFilter = useStore($stackFilter)
const containerColors = useStore($containerColors)
const filter = useStore($containerFilter)
const userSettings = useStore($userSettings)
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { containerData } = chartData
const isNetChart = chartType === ChartType.Network
const isVolumeChart = chartType === ChartType.Volume
const isHealthChart = chartType === ChartType.Health
const isUptimeChart = chartType === ChartType.Uptime
const isHealthUptimeChart = chartType === ChartType.HealthUptime
const isDiskIOChart = chartType === ChartType.DiskIO
// Centralized data processing for all chart types
const chartDatasets = useMemo(() => {
const volumeChartData = { data: [], colors: {} } as {
data: Record<string, number | string>[]
colors: Record<string, string>
}
const healthChartData = { data: [], colors: {} } as {
data: Record<string, number | string>[]
colors: Record<string, string>
}
const uptimeChartData = { data: [], colors: {} } as {
data: Record<string, number | string>[]
colors: Record<string, string>
}
const healthUptimeChartData = { data: [], colors: {} } as {
data: Record<string, number | string>[]
colors: Record<string, string>
}
const containerChartConfig = {} as Record<string, { label: string; color: string }>
const volumeSums: Record<string, number> = {}
const volumeContainers: Record<string, string[]> = {}
const allContainerNames = new Set<string>()
const healthUptimeContainerNames = new Set<string>()
for (const containerStats of containerData) {
if (!containerStats.created) {
// For gaps in data
volumeChartData.data.push({ created: "" })
healthChartData.data.push({ created: "" })
uptimeChartData.data.push({ created: "" })
healthUptimeChartData.data.push({ created: "" })
continue
}
const volumeData = { created: containerStats.created } as Record<string, number | string>
const healthData = { created: containerStats.created } as Record<string, number | string>
const uptimeData = { created: containerStats.created } as Record<string, number | string>
const healthUptimeData = { created: containerStats.created } as Record<string, number | string>
for (const [containerName, containerDataObj] of Object.entries(containerStats)) {
if (containerName === "created") continue
// Apply container filter
if (containerFilter.length > 0 && !containerFilter.includes(containerName)) {
continue
}
// Apply stack filter
if (stackFilter.length > 0 && typeof containerDataObj === "object" && containerDataObj) {
const stackName = (containerDataObj as any).p || "—"
if (!stackFilter.includes(stackName)) {
continue
}
}
allContainerNames.add(containerName)
if (typeof containerDataObj === "object" && containerDataObj) {
// Volume
if ("v" in containerDataObj && containerDataObj.v) {
for (const [volumeName, volumeSize] of Object.entries(containerDataObj.v)) {
if (typeof volumeSize === "number" && volumeSize > 0) {
volumeData[volumeName] = ((volumeData[volumeName] as number) || 0) + volumeSize
volumeSums[volumeName] = (volumeSums[volumeName] ?? 0) + volumeSize
if (!volumeContainers[volumeName]) volumeContainers[volumeName] = []
if (!volumeContainers[volumeName].includes(containerName))
volumeContainers[volumeName].push(containerName)
}
}
}
// Health
if ("h" in containerDataObj) {
const healthStatus = ((containerDataObj.h as string) || "").toLowerCase()
let healthValue = 0
switch (healthStatus) {
case "healthy":
healthValue = 3
break
case "starting":
healthValue = 2
break
case "unhealthy":
healthValue = 1
break
default:
healthValue = 0
}
healthData[containerName] = healthValue
// Health+Uptime
healthUptimeData[`${containerName}_health`] = healthValue
healthUptimeContainerNames.add(containerName)
}
// Uptime
if ("u" in containerDataObj && containerDataObj.u) {
uptimeData[containerName] = (containerDataObj.u as number) / 3600
// Health+Uptime
healthUptimeData[`${containerName}_uptime`] = (containerDataObj.u as number) / 3600
healthUptimeContainerNames.add(containerName)
}
}
}
volumeChartData.data.push(volumeData)
healthChartData.data.push(healthData)
uptimeChartData.data.push(uptimeData)
healthUptimeChartData.data.push(healthUptimeData)
}
// Only process volumes attached to containers
const volumeKeys = Object.keys(volumeSums)
.filter((key) => (volumeContainers[key] || []).length > 0)
.sort((a, b) => volumeSums[b] - volumeSums[a])
for (const key of volumeKeys) {
const containers = volumeContainers[key] || []
const firstContainer = containers[0]
volumeChartData.colors[key] =
containerColors[firstContainer] || generateFallbackColor(firstContainer)
}
const healthKeys = Object.keys(healthChartData.data[0] || {}).filter((key) => key !== "created")
for (const key of healthKeys) {
healthChartData.colors[key] = containerColors[key] || generateFallbackColor(key)
}
const uptimeKeys = Object.keys(uptimeChartData.data[0] || {}).filter((key) => key !== "created")
for (const key of uptimeKeys) {
uptimeChartData.colors[key] = containerColors[key] || generateFallbackColor(key)
}
for (const containerName of healthUptimeContainerNames) {
const color = containerColors[containerName] || generateFallbackColor(containerName)
healthUptimeChartData.colors[`${containerName}_uptime`] = color
healthUptimeChartData.colors[`${containerName}_health`] = color
}
for (const containerName of allContainerNames) {
const color = containerColors[containerName] || generateFallbackColor(containerName)
containerChartConfig[containerName] = { label: containerName, color }
}
return {
volumeChartData,
healthChartData,
uptimeChartData,
healthUptimeChartData,
containerChartConfig,
}
}, [containerData, containerColors, containerFilter, stackFilter])
const { volumeChartData, healthChartData, uptimeChartData, healthUptimeChartData, containerChartConfig } =
chartDatasets
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
@@ -208,40 +41,9 @@ export default memo(function ContainerChart({
// tick formatter
if (chartType === ChartType.CPU) {
obj.tickFormatter = (value) => {
const val = `${toFixedWithoutTrailingZeros(value, 2)}${unit}`
const val = toFixedFloat(value, 2) + unit
return updateYAxisWidth(val)
}
} else if (isHealthChart) {
obj.tickFormatter = (value) => {
let healthLabel = "Unknown"
switch (value) {
case 3:
healthLabel = "Healthy"
break
case 2:
healthLabel = "Starting"
break
case 1:
healthLabel = "Unhealthy"
break
case 0:
healthLabel = "None"
break
}
return updateYAxisWidth(healthLabel)
}
} else if (isUptimeChart) {
obj.tickFormatter = (value) => {
const hours = Math.floor(value)
const minutes = Math.floor((value - hours) * 60)
const label = `${hours}h ${minutes}m`
return updateYAxisWidth(label)
}
} else if (isVolumeChart || isDiskIOChart) {
obj.tickFormatter = (value) => {
const { v, u } = getSizeAndUnit(value, false)
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isDiskIOChart ? "/s" : ""}`)
}
} else {
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
obj.tickFormatter = (val) => {
@@ -255,12 +57,7 @@ export default memo(function ContainerChart({
try {
const sent = item?.payload?.[key]?.ns ?? 0
const received = item?.payload?.[key]?.nr ?? 0
const { value: receivedValue, unit: receivedUnit } = formatBytes(
received,
true,
userSettings.unitNet,
true
)
const { value: receivedValue, unit: receivedUnit } = formatBytes(received, true, userSettings.unitNet, true)
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, true)
return (
<span className="flex">
@@ -275,74 +72,17 @@ export default memo(function ContainerChart({
return null
}
}
} else if (isDiskIOChart) {
obj.toolTipFormatter = (item: any, key: string) => {
try {
const read = item?.payload?.[key]?.dr ?? 0
const write = item?.payload?.[key]?.dw ?? 0
return (
<span className="flex">
{decimalString(read)} MB/s
<span className="opacity-70 ms-0.5"> read </span>
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
{decimalString(write)} MB/s
<span className="opacity-70 ms-0.5"> write</span>
</span>
)
} catch (e) {
return null
}
}
} else if (chartType === ChartType.Memory) {
obj.toolTipFormatter = (item: any) => {
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
return `${decimalString(value)} ${unit}`
}
} else if (isVolumeChart) {
obj.toolTipFormatter = (item: any) => {
const { v, u } = getSizeAndUnit(item.value, false)
return `${decimalString(v, 2)}${u}`
}
} else if (isHealthChart) {
obj.toolTipFormatter = (item: any) => {
let healthLabel = "Unknown"
switch (item.value) {
case 3:
healthLabel = "Healthy"
break
case 2:
healthLabel = "Starting"
break
case 1:
healthLabel = "Unhealthy"
break
case 0:
healthLabel = "None"
break
}
return healthLabel
}
} else if (isUptimeChart) {
obj.toolTipFormatter = (item: any) => {
const hours = Math.floor(item.value)
const minutes = Math.floor((item.value - hours) * 60)
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
if (days > 0) {
return `${days}d ${remainingHours}h ${minutes}m`
}
return `${hours}h ${minutes}m`
}
} else {
obj.toolTipFormatter = (item: any) => `${decimalString(item.value)} ${unit}`
}
// data function
if (isNetChart) {
obj.dataFunction = (key: string, data: any) => (data[key] ? data[key].nr + data[key].ns : null)
} else if (isDiskIOChart) {
obj.dataFunction = (key: string, data: any) =>
data[key] ? (data[key].dr || 0) + (data[key].dw || 0) : null
} else {
obj.dataFunction = (key: string, data: any) => data[key]?.[dataKey] ?? null
}
@@ -351,11 +91,15 @@ export default memo(function ContainerChart({
// Filter with set lookup
const filteredKeys = useMemo(() => {
if (!containerFilter || containerFilter.length === 0) {
if (!filter) {
return new Set<string>()
}
return new Set(Object.keys(containerChartConfig).filter((key) => !containerFilter.includes(key)))
}, [containerChartConfig, containerFilter])
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())
@@ -363,228 +107,8 @@ export default memo(function ContainerChart({
return null
}
// For volume charts, check if we have volume data
if (isVolumeChart) {
if (!volumeChartData || Object.keys(volumeChartData.colors).length === 0) {
return null
}
}
// For health charts, check if we have health data
if (isHealthChart) {
if (!healthChartData || Object.keys(healthChartData.colors).length === 0) {
return null
}
}
// For uptime charts, check if we have uptime data
if (isUptimeChart) {
if (!uptimeChartData || Object.keys(uptimeChartData.colors).length === 0) {
return null
}
}
// For combined health+uptime chart
if (isHealthUptimeChart) {
if (!healthUptimeChartData || healthUptimeChartData.data.length === 0) return null
return (
<HealthUptimeTable
containerData={containerData}
healthUptimeChartData={healthUptimeChartData}
containerColors={containerColors}
filter={containerFilter}
/>
)
}
// Only show selected containers, or all if none selected
const filterableKeys = isVolumeChart
? Object.keys(containerChartConfig)
: Object.keys(containerChartConfig).filter(
(key) =>
!(
containerChartConfig[key] &&
containerChartConfig[key].label &&
containerChartConfig[key].label.startsWith("(orphaned volume)")
)
)
// Render volume chart
if (isVolumeChart) {
const colors = Object.keys(volumeChartData!.colors)
return (
<div className="w-full h-full">
<ChartContainer
className={cn("h-full w-full absolute bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<AreaChart
accessibilityLayer
data={volumeChartData!.data}
margin={chartMargin}
reverseStackOrder={true}
>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
domain={[0, "auto"]}
width={yAxisWidth}
tickFormatter={(value) => {
const { v, u } = getSizeAndUnit(value, false)
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}`)
}}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
truncate={true}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={toolTipFormatter}
/>
}
/>
{colors.map((key) => (
<Area
key={key}
dataKey={key}
name={key}
type="monotoneX"
fill={volumeChartData!.colors[key]}
fillOpacity={0.4}
stroke={volumeChartData!.colors[key]}
strokeOpacity={1}
activeDot={{ opacity: 1 }}
stackId="a"
isAnimationActive={false}
/>
))}
</AreaChart>
</ChartContainer>
</div>
)
}
// Render health chart
if (isHealthChart) {
const colors = Object.keys(healthChartData!.colors)
return (
<div className="w-full h-full">
<ChartContainer
className={cn("h-full w-full absolute bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<LineChart accessibilityLayer data={healthChartData!.data} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
domain={[0, 3]}
width={yAxisWidth}
tickFormatter={tickFormatter}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
truncate={true}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={toolTipFormatter}
/>
}
/>
{colors.map((key) => (
<Line
key={key}
dataKey={key}
name={key}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={healthChartData!.colors[key]}
isAnimationActive={false}
/>
))}
</LineChart>
</ChartContainer>
</div>
)
}
// Render uptime chart
if (isUptimeChart) {
const colors = Object.keys(uptimeChartData!.colors)
return (
<div className="w-full h-full">
<ChartContainer
className={cn("h-full w-full absolute bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
})}
>
<LineChart accessibilityLayer data={uptimeChartData!.data} margin={chartMargin}>
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
orientation={chartData.orientation}
className="tracking-tighter"
domain={[0, "auto"]}
width={yAxisWidth}
tickFormatter={tickFormatter}
tickLine={false}
axisLine={false}
/>
{xAxis(chartData)}
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
truncate={true}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={toolTipFormatter}
/>
}
/>
{colors.map((key) => (
<Line
key={key}
dataKey={key}
name={key}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={uptimeChartData!.colors[key]}
isAnimationActive={false}
/>
))}
</LineChart>
</ChartContainer>
</div>
)
}
// Render regular container chart (Area chart)
return (
<div className="w-full h-full">
<div>
<ChartContainer
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
"opacity-100": yAxisWidth,
@@ -615,14 +139,9 @@ export default memo(function ContainerChart({
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
// @ts-expect-error
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
filter={containerFilter.length > 0 ? containerFilter.join(",") : undefined}
contentFormatter={toolTipFormatter}
/>
}
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} showTotal={true} />}
/>
{filterableKeys.map((key) => {
{Object.keys(chartConfig).map((key) => {
const filtered = filteredKeys.has(key)
const fillOpacity = filtered ? 0.05 : 0.4
const strokeOpacity = filtered ? 0.1 : 1
@@ -633,11 +152,11 @@ export default memo(function ContainerChart({
dataKey={dataFunction.bind(null, key)}
name={key}
type="monotoneX"
fill={containerChartConfig[key].color}
fill={chartConfig[key].color}
fillOpacity={fillOpacity}
stroke={containerChartConfig[key].color}
stroke={chartConfig[key].color}
strokeOpacity={strokeOpacity}
activeDot={{ opacity: 1 }}
activeDot={{ opacity: filtered ? 0 : 1 }}
stackId="a"
/>
)
@@ -647,358 +166,3 @@ export default memo(function ContainerChart({
</div>
)
})
// Extracted HealthUptimeTable component
const HealthUptimeTable = React.memo(function HealthUptimeTable({
containerData,
healthUptimeChartData,
containerColors,
filter,
}: {
containerData: any[]
healthUptimeChartData: { data: any[]; colors: Record<string, string> }
containerColors: Record<string, string>
filter: string[]
}) {
const stackFilter = useStore($stackFilter)
// Get the latest data point for table display
const latestData = healthUptimeChartData.data[healthUptimeChartData.data.length - 1]
if (!latestData) return null
// Extract container data for table
const containerTableData = React.useMemo(() => {
const containerNames = new Set<string>()
for (const key of Object.keys(latestData)) {
if (key === "created") continue
const containerName = key.replace(/_uptime$/, "").replace(/_health$/, "")
// Skip orphaned volumes
if (containerName.startsWith("(orphaned volume)")) continue
containerNames.add(containerName)
}
const tableData = []
for (const containerName of containerNames) {
const uptimeKey = `${containerName}_uptime`
const healthKey = `${containerName}_health`
const uptimeHours = latestData[uptimeKey]
const healthValue = latestData[healthKey] || 0
let healthStatus = "Unknown"
switch (healthValue) {
case 3:
healthStatus = "Healthy"
break
case 2:
healthStatus = "Starting"
break
case 1:
healthStatus = "Unhealthy"
break
case 0:
healthStatus = "None"
break
}
let uptimeDisplay = "N/A"
if (typeof uptimeHours === "number" && !Number.isNaN(uptimeHours)) {
const hours = Math.floor(uptimeHours)
const minutes = Math.floor((uptimeHours - hours) * 60)
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
if (days > 0) {
uptimeDisplay = `${days}d ${remainingHours}h ${minutes}m`
} else {
uptimeDisplay = `${hours}h ${minutes}m`
}
}
let stackName = "—"
let statusInfo = "—"
let idShort = ""
for (let i = containerData.length - 1; i >= 0; i--) {
const containerStats = containerData[i]
if (containerStats.created && containerStats[containerName]) {
const containerDataObj = containerStats[containerName]
if (typeof containerDataObj === "object" && containerDataObj) {
if ("p" in containerDataObj) {
stackName = containerDataObj.p as string
}
if ("s" in containerDataObj) {
statusInfo = containerDataObj.s as string
}
if ("idShort" in containerDataObj) {
idShort = containerDataObj.idShort as string
}
break
}
}
}
tableData.push({
name: containerName,
idShort,
health: healthStatus,
status: statusInfo,
uptime: uptimeDisplay,
uptimeHours: uptimeHours,
healthValue: healthValue,
stack: stackName,
color: containerColors[containerName] || generateFallbackColor(containerName),
})
}
return tableData
}, [containerData, latestData, containerColors])
// Sort and filter state
const [sortField, setSortField] = React.useState<"name" | "idShort" | "stack" | "health" | "status" | "uptime">(
"uptime"
)
const [sortDirection, setSortDirection] = React.useState<"asc" | "desc">("desc")
const [currentPage, setCurrentPage] = React.useState(1)
const containersPerPage = 4
// Filtered data
const filteredContainerData = React.useMemo(() => {
let filtered = containerTableData
// Apply container filter
if (Array.isArray(filter) && filter.length > 0) {
filtered = filtered.filter((container) => filter.includes(container.name))
}
// Apply stack filter
if (Array.isArray(stackFilter) && stackFilter.length > 0) {
filtered = filtered.filter((container) => stackFilter.includes(container.stack))
}
return filtered
}, [containerTableData, filter, stackFilter])
// Sorted data
const sortedContainerData = React.useMemo(() => {
return [...filteredContainerData].sort((a, b) => {
let aValue: string | number
let bValue: string | number
switch (sortField) {
case "name":
aValue = a.name?.toLowerCase?.() || ""
bValue = b.name?.toLowerCase?.() || ""
break
case "idShort":
aValue = a.idShort || ""
bValue = b.idShort || ""
break
case "stack":
aValue = a.stack?.toLowerCase?.() || ""
bValue = b.stack?.toLowerCase?.() || ""
break
case "health":
aValue = typeof a.healthValue === "number" ? a.healthValue : 0
bValue = typeof b.healthValue === "number" ? b.healthValue : 0
break
case "status":
aValue = a.status?.toLowerCase?.() || ""
bValue = b.status?.toLowerCase?.() || ""
break
case "uptime":
aValue = typeof a.uptimeHours === "number" ? a.uptimeHours : 0
bValue = typeof b.uptimeHours === "number" ? b.uptimeHours : 0
break
default:
return 0
}
if (aValue < bValue) return sortDirection === "asc" ? -1 : 1
if (aValue > bValue) return sortDirection === "asc" ? 1 : -1
return 0
})
}, [filteredContainerData, sortField, sortDirection])
// Pagination
const totalPages = Math.ceil(sortedContainerData.length / containersPerPage)
const startIndex = (currentPage - 1) * containersPerPage
const endIndex = startIndex + containersPerPage
const currentContainers = sortedContainerData.slice(startIndex, endIndex)
// Handlers
const handleSort = (field: "name" | "idShort" | "stack" | "health" | "status" | "uptime") => {
if (sortField === field) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc")
} else {
setSortField(field)
setSortDirection("asc")
}
}
const getSortIcon = (field: "name" | "idShort" | "stack" | "health" | "status" | "uptime") => {
if (sortField !== field) return "↑↓"
return sortDirection === "asc" ? "↑" : "↓"
}
React.useEffect(() => {
setCurrentPage(1)
}, [filteredContainerData, sortField, sortDirection])
return (
<div className="w-full h-full flex flex-col opacity-100">
<div className="flex-1 p-2 overflow-hidden">
<div className="overflow-x-auto h-full">
<table className="w-full text-xs table-fixed">
<thead>
<tr className="border-b border-border">
<th
className="text-left font-medium p-1 w-1/6 cursor-pointer hover:bg-muted/50 transition-colors select-none"
onClick={() => handleSort("idShort")}
>
<div className="flex items-center gap-1">
ID
<span className="text-xs opacity-60">{getSortIcon("idShort")}</span>
</div>
</th>
<th
className="text-left font-medium p-1 w-1/4 cursor-pointer hover:bg-muted/50 transition-colors select-none"
onClick={() => handleSort("name")}
>
<div className="flex items-center gap-1">
Container
<span className="text-xs opacity-60">{getSortIcon("name")}</span>
</div>
</th>
<th
className="text-left font-medium p-1 w-1/6 cursor-pointer hover:bg-muted/50 transition-colors select-none"
onClick={() => handleSort("stack")}
>
<div className="flex items-center gap-1">
Stack
<span className="text-xs opacity-60">{getSortIcon("stack")}</span>
</div>
</th>
<th
className="text-left font-medium p-1 w-1/6 cursor-pointer hover:bg-muted/50 transition-colors select-none"
onClick={() => handleSort("health")}
>
<div className="flex items-center gap-1">
Health
<span className="text-xs opacity-60">{getSortIcon("health")}</span>
</div>
</th>
<th
className="text-left font-medium p-1 w-1/6 cursor-pointer hover:bg-muted/50 transition-colors select-none"
onClick={() => handleSort("status")}
>
<div className="flex items-center gap-1">
Status
<span className="text-xs opacity-60">{getSortIcon("status")}</span>
</div>
</th>
<th
className="text-left font-medium p-1 w-1/6 cursor-pointer hover:bg-muted/50 transition-colors select-none"
onClick={() => handleSort("uptime")}
>
<div className="flex items-center gap-1">
Uptime
<span className="text-xs opacity-60">{getSortIcon("uptime")}</span>
</div>
</th>
</tr>
</thead>
<tbody>
{currentContainers.map((container) => (
<tr key={container.name} className="border-b border-border/30 hover:bg-muted/30">
<td className="p-1 w-1/6 font-mono text-xs text-muted-foreground" title={container.idShort}>
{container.idShort}
</td>
<td className="p-1 w-1/4">
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: container.color }} />
<span className="text-xs truncate">{container.name}</span>
</div>
</td>
<td className="p-1 w-1/6">
<span className="text-xs text-muted-foreground truncate" title={container.stack}>
{container.stack}
</span>
</td>
<td className="p-1 w-1/6">
<Badge
className={cn("px-1.5 py-0.5 text-xs font-medium whitespace-nowrap border-0", {
"bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400":
container.healthValue === 3,
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400":
container.healthValue === 2,
"bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400": container.healthValue === 1,
"bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400": container.healthValue === 0,
})}
>
{container.health}
</Badge>
</td>
<td className="p-1 w-1/6">
<Badge
className={cn("px-1.5 py-0.5 text-xs font-medium whitespace-nowrap border-0", {
"bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400":
container.status?.toLowerCase() === "running",
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400":
container.status?.toLowerCase() === "paused" || container.status?.toLowerCase() === "restarting",
"bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400":
container.status?.toLowerCase().includes("exited") ||
container.status?.toLowerCase().includes("dead") ||
container.status?.toLowerCase().includes("removing"),
"bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400":
!container.status || container.status?.toLowerCase() === "created",
})}
title={container.status}
>
{container.status}
</Badge>
</td>
<td className="p-1 w-1/6 text-xs whitespace-nowrap">{container.uptime}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-2 py-2 border-t border-border bg-muted/20">
<div className="text-xs text-muted-foreground">
Showing {startIndex + 1}-{Math.min(endIndex, sortedContainerData.length)} of{" "}
{sortedContainerData.length} containers
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className={cn(
"px-2 py-1 text-xs rounded border transition-colors",
"hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed",
"border-border hover:border-border/60"
)}
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={cn(
"px-2 py-1 text-xs rounded border transition-colors min-w-[28px]",
page === currentPage
? "bg-primary text-primary-foreground border-primary"
: "border-border hover:bg-muted hover:border-border/60"
)}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className={cn(
"px-2 py-1 text-xs rounded border transition-colors",
"hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed",
"border-border hover:border-border/60"
)}
>
Next
</button>
</div>
</div>
)}
</div>
)
})

View File

@@ -1,257 +0,0 @@
"use client"
import * as React from "react"
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
export type DockerHealthRow = {
id: string
name: string
stack: string
health: string
healthValue: number
status: string
uptime: string
color: string
}
type Props = {
data: DockerHealthRow[]
}
export function ContainerHealthTable({ data }: Props) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const columns = React.useMemo<ColumnDef<DockerHealthRow>[]>(() => [
{
accessorKey: "id",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
ID
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<span className="font-mono text-xs text-muted-foreground" title={row.original.id}>{row.original.id}</span>
),
},
{
accessorKey: "name",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Container
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: row.original.color }} />
<span className="text-xs truncate">{row.original.name}</span>
</div>
),
},
{
accessorKey: "stack",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Stack
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<span className="text-xs text-muted-foreground truncate" title={row.original.stack}>{row.original.stack}</span>
),
},
{
accessorKey: "health",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Health
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<Badge className={cn(
"px-1.5 py-0.5 text-xs font-medium whitespace-nowrap border-0",
{
"bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400": row.original.healthValue === 3,
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400": row.original.healthValue === 2,
"bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400": row.original.healthValue === 1,
"bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400": row.original.healthValue === 0,
}
)}>{row.original.health}</Badge>
),
},
{
accessorKey: "status",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<Badge className={cn(
"px-1.5 py-0.5 text-xs font-medium whitespace-nowrap border-0",
{
"bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400": row.original.status?.toLowerCase() === "running",
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400": row.original.status?.toLowerCase() === "paused" || row.original.status?.toLowerCase() === "restarting",
"bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400":
row.original.status?.toLowerCase().includes("exited") ||
row.original.status?.toLowerCase().includes("dead") ||
row.original.status?.toLowerCase().includes("removing"),
"bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400": !row.original.status || row.original.status?.toLowerCase() === "created",
}
)} title={row.original.status}>{row.original.status}</Badge>
),
},
{
accessorKey: "uptime",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Uptime
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<span className="text-xs whitespace-nowrap">{row.original.uptime}</span>
),
},
], [])
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnVisibility,
rowSelection,
},
})
React.useEffect(() => {
table.setPageSize(5)
}, [table])
return (
<div className="w-full">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{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"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-1">
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
)
}

View File

@@ -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))`,
}
}

View File

@@ -61,6 +61,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
}}
showTotal={true}
/>
}
/>

View File

@@ -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>
)
})
})

View File

@@ -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" }))

View File

@@ -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>
)
}

View File

@@ -0,0 +1,532 @@
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 loadTime = Date.now()
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(() => {
function fetchData(systemId?: string) {
pb.collection<ContainerRecord>("containers")
.getList(0, 2000, {
fields: "id,name,image,cpu,memory,net,health,status,system,updated",
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
})
.then(({ items }) => items.length && setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const containerIds = new Set()
const newItems = []
for (const item of items) {
if (Math.abs(lastUpdated - item.updated) < 70_000) {
containerIds.add(item.id)
newItems.push(item)
}
}
for (const item of curItems) {
if (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
}))
}
// initial load
fetchData(systemId)
// if no systemId, pull system containers after every system update
if (!systemId) {
return $allSystemsById.listen((_value, _oldValue, systemId) => {
// exclude initial load of systems
if (Date.now() - loadTime > 500) {
fetchData(systemId)
}
})
}
// if systemId, fetch containers after the system is updated
return listenKeys($allSystemsById, [systemId], (_newSystems) => {
fetchData(systemId)
})
}, [])
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>
)
}

View 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>
)
}

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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>
)

View File

@@ -2,6 +2,7 @@ import { createRouter } from "@nanostores/router"
const routes = {
home: "/",
containers: "/containers",
system: `/system/:id`,
settings: `/settings/:name?`,
forgot_password: `/forgot-password`,

View 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 />
</>
),
[]
)
})

View File

@@ -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("")])
}

View File

@@ -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>

View File

@@ -1,21 +1,19 @@
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"
import {
ChevronRightSquareIcon,
ClockArrowUp,
Container as ContainerIcon,
CpuIcon,
GlobeIcon,
LayoutGridIcon,
MonitorIcon,
Server as ServerIcon,
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"
@@ -32,11 +30,9 @@ import {
$allSystemsById,
$allSystemsByName,
$chartTime,
$containerColors,
$containerFilter,
$direction,
$maxValues,
$stackFilter,
$systems,
$temperatureFilter,
$userSettings,
@@ -45,9 +41,11 @@ import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import {
chartTimeData,
cn,
compareSemVer,
debounce,
decimalString,
formatBytes,
secondsToString,
getHostDisplayValue,
listen,
parseSemVer,
@@ -73,11 +71,13 @@ import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocke
import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
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: {
@@ -98,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 = {
@@ -173,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)
@@ -219,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
}
@@ -360,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 },
@@ -400,76 +392,20 @@ export default memo(function SystemDetail({ id }: { id: string }) {
}[]
}, [system, t])
/** Space for tooltip if more than 12 containers */
// biome-ignore lint/correctness/useExhaustiveDependencies: filters accessed via .get()
/** Space for tooltip if more than 10 sensors and no containers table */
useEffect(() => {
const calculateSpacing = () => {
if (!netCardRef.current || !containerData.length) {
setBottomSpacing(0)
return
}
// Count visible containers after applying filters
const containerFilter = $containerFilter.get()
const stackFilter = $stackFilter.get()
let visibleCount = 0
if (containerData[0]) {
for (const [key, value] of Object.entries(containerData[0])) {
if (key === "created") continue
// Apply container filter
if (containerFilter.length > 0 && !containerFilter.includes(key)) {
continue
}
// Apply stack filter
if (stackFilter.length > 0 && typeof value === "object" && value) {
const stackName = (value as any).p || "—"
if (!stackFilter.includes(stackName)) {
continue
}
}
visibleCount++
}
}
// Only add spacing if there are more than 12 visible containers
if (visibleCount > 12) {
const tooltipHeight = (visibleCount - 11) * 17.8 - 40
const wrapperEl = chartWrapRef.current as HTMLDivElement
const wrapperRect = wrapperEl.getBoundingClientRect()
const chartRect = netCardRef.current.getBoundingClientRect()
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
const spacing = Math.max(0, tooltipHeight - distanceToBottom)
setBottomSpacing(spacing)
} else {
setBottomSpacing(0)
}
const sensors = Object.keys(systemStats.at(-1)?.stats.t ?? {})
if (!temperatureChartRef.current || sensors.length < 10 || containerData.length > 0) {
setBottomSpacing(0)
return
}
// Initial calculation
calculateSpacing()
// Subscribe to filter changes to recalculate
// Use requestAnimationFrame to wait for chart re-render, then add small delay for layout
const unsubContainer = $containerFilter.subscribe(() => {
requestAnimationFrame(() => {
setTimeout(calculateSpacing, 100)
})
})
const unsubStack = $stackFilter.subscribe(() => {
requestAnimationFrame(() => {
setTimeout(calculateSpacing, 100)
})
})
return () => {
unsubContainer()
unsubStack()
}
}, [containerData])
const tooltipHeight = (sensors.length - 10) * 17.8 - 40
const wrapperEl = chartWrapRef.current as HTMLDivElement
const wrapperRect = wrapperEl.getBoundingClientRect()
const chartRect = temperatureChartRef.current.getBoundingClientRect()
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
setBottomSpacing(tooltipHeight - distanceToBottom)
}, [])
// keyboard navigation between systems
useEffect(() => {
@@ -516,14 +452,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const showMax = maxValues && isLongerChart
const containerFilterBar = containerData.length ? <FilterBar containerData={containerData} /> : null
const stackFilterBar = containerData.length ? <FilterBar containerData={containerData} store={$stackFilter} mode="stack" /> : null
const combinedFilterBar = containerData.length ? (
<div className="flex gap-2">
<FilterBar containerData={containerData} />
<FilterBar containerData={containerData} store={$stackFilter} mode="stack" />
</div>
) : null
const containerFilterBar = containerData.length ? <FilterBar /> : null
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
@@ -639,21 +568,17 @@ export default memo(function SystemDetail({ id }: { id: string }) {
</Card>
{/* Tabbed interface for system and Docker stats */}
<Tabs defaultValue="system" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="system" className="flex items-center gap-2">
<ServerIcon className="h-4 w-4" />
{t`System Stats`}
</TabsTrigger>
<TabsTrigger value="docker" className="flex items-center gap-2">
<ContainerIcon className="h-4 w-4" />
{dockerOrPodman(t`Docker Stats`, system)}
</TabsTrigger>
</TabsList>
{/* <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> */}
{/* System Stats Tab */}
<TabsContent value="system" className="space-y-4">
{/* main charts */}
<div className="grid xl:grid-cols-2 gap-4">
<ChartCard
@@ -661,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}
@@ -679,6 +609,23 @@ export default memo(function SystemDetail({ id }: { id: string }) {
/>
</ChartCard>
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker CPU Usage`, system)}
description={t`Average CPU utilization of containers`}
cornerEl={containerFilterBar}
>
<ContainerChart
chartData={chartData}
dataKey="c"
chartType={ChartType.CPU}
chartConfig={containerChartConfigs.cpu}
/>
</ChartCard>
)}
<ChartCard
empty={dataEmpty}
grid={grid}
@@ -689,6 +636,23 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<MemChart chartData={chartData} showMax={showMax} />
</ChartCard>
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Memory Usage`, system)}
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
cornerEl={containerFilterBar}
>
<ContainerChart
chartData={chartData}
dataKey="m"
chartType={ChartType.Memory}
chartConfig={containerChartConfigs.memory}
/>
</ChartCard>
)}
<ChartCard empty={dataEmpty} grid={grid} title={t`Disk Usage`} description={t`Usage of root partition`}>
<DiskChart chartData={chartData} dataKey="stats.du" diskSize={systemStats.at(-1)?.stats.d ?? NaN} />
</ChartCard>
@@ -735,6 +699,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
showTotal={true}
/>
</ChartCard>
@@ -788,9 +753,27 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`
}}
showTotal={true}
/>
</ChartCard>
{containerFilterBar && containerData.length > 0 && (
<ChartCard
empty={dataEmpty}
grid={grid}
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>
)}
{/* Swap chart */}
{(systemStats.at(-1)?.stats.su ?? 0) > 0 && (
<ChartCard
@@ -818,16 +801,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 */}
@@ -978,9 +966,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,
@@ -1015,128 +1003,17 @@ export default memo(function SystemDetail({ id }: { id: string }) {
})}
</div>
)}
</TabsContent>
{/* Docker Stats Tab */}
<TabsContent value="docker" className="space-y-4">
{/* Centralized filter bar for all Docker charts */}
{combinedFilterBar && (
<div className="flex justify-end gap-2 pb-2">
{combinedFilterBar}
</div>
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && (
<LazySmartTable systemId={system.id} />
)}
<div className="grid xl:grid-cols-2 gap-4">
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker CPU Usage`, system)}
description={t`Average CPU utilization of containers`}
>
<ContainerChart
chartData={chartData}
dataKey="c"
chartType={ChartType.CPU}
chartConfig={containerChartConfigs.cpu}
/>
</ChartCard>
)}
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Memory Usage`, system)}
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
>
<ContainerChart
chartData={chartData}
dataKey="m"
chartType={ChartType.Memory}
chartConfig={containerChartConfigs.memory}
/>
</ChartCard>
)}
{containerFilterBar && containerData.length > 0 && (
<div
ref={netCardRef}
className={cn({
"col-span-full": !grid,
})}
>
<ChartCard
empty={dataEmpty}
title={dockerOrPodman(t`Docker Network I/O`, system)}
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
>
<ContainerChart
chartData={chartData}
chartType={ChartType.Network}
dataKey="n"
chartConfig={containerChartConfigs.network}
/>
</ChartCard>
</div>
)}
{/* Docker Disk I/O chart */}
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Disk I/O`, system)}
description={dockerOrPodman(t`Disk read/write rates of docker containers`, system)}
>
<ContainerChart
chartData={chartData}
chartType={ChartType.DiskIO}
dataKey="d"
chartConfig={{}}
unit=" MB/s"
/>
</ChartCard>
)}
{/* Docker Volumes chart */}
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Volumes`, system)}
description={dockerOrPodman(t`Volume usage of docker containers`, system)}
>
<ContainerChart
chartData={chartData}
chartType={ChartType.Volume}
dataKey="v"
chartConfig={{}}
/>
</ChartCard>
)}
{/* Docker Health & Uptime chart */}
{containerFilterBar && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Health & Uptime`, system)}
description={dockerOrPodman(t`Container health status and uptime`, system)}
>
<ContainerChart
chartData={chartData}
chartType={ChartType.HealthUptime}
dataKey="h"
chartConfig={{}}
/>
</ChartCard>
)}
</div>
</TabsContent>
</Tabs>
{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 }} />}
</>
)
@@ -1164,121 +1041,38 @@ function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
)
}
function FilterBar({
store = $containerFilter,
containerData,
mode = "container"
}: {
store?: typeof $containerFilter
containerData?: ChartData["containerData"]
mode?: "container" | "stack"
}) {
const selected = useStore(store)
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
const containerFilter = useStore(store)
const { t } = useLingui()
const [open, setOpen] = useState(false)
// Extract all unique container names or stack names from current data
const availableItems = useMemo(() => {
const items = new Set<string>()
if (containerData) {
for (const dataPoint of containerData) {
if (dataPoint.created) {
for (const [key, value] of Object.entries(dataPoint)) {
if (key !== "created") {
if (mode === "stack" && typeof value === "object" && value) {
// Extract stack/project name
const stackName = (value as any).p || "—"
items.add(stackName)
} else if (mode === "container") {
// Add container name
items.add(key)
}
}
}
}
}
}
return Array.from(items).sort()
}, [containerData, mode])
const debouncedStoreSet = useMemo(() => debounce((value: string) => store.set(value), 80), [store])
const toggleItem = useCallback((item: string) => {
const current = store.get()
if (current.includes(item)) {
store.set(current.filter(i => i !== item))
} else {
store.set([...current, item])
}
}, [store])
const clearAll = useCallback(() => {
store.set([])
setOpen(false)
}, [store])
// Close dropdown when clicking outside
useEffect(() => {
if (!open) return
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('[data-filter-dropdown]')) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [open])
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => debouncedStoreSet(e.target.value),
[debouncedStoreSet]
)
return (
<div className="relative" data-filter-dropdown>
<Button
variant="outline"
size="sm"
className="h-9 w-full sm:w-44 justify-between"
onClick={() => setOpen(!open)}
>
<span className="truncate">
{selected.length === 0
? (mode === "stack" ? t`Filter stacks...` : t`Filter containers...`)
: `${selected.length} selected`
}
</span>
<ChevronRightSquareIcon className="ml-2 h-4 w-4 opacity-50" />
</Button>
{open && (
<div className="absolute z-50 mt-1 w-full sm:w-64 bg-popover border rounded-md shadow-md">
<div className="p-2 space-y-1 max-h-64 overflow-y-auto">
{availableItems.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{t`No items available`}
</div>
) : (
availableItems.map((item) => (
<div
key={item}
className="flex items-center space-x-2 rounded-sm px-2 py-1.5 cursor-pointer hover:bg-accent"
onClick={() => toggleItem(item)}
>
<input
type="checkbox"
checked={selected.includes(item)}
onChange={() => {}}
className="h-4 w-4"
/>
<span className="text-sm truncate flex-1">{item}</span>
</div>
))
)}
</div>
{selected.length > 0 && (
<div className="border-t p-2">
<Button variant="ghost" size="sm" className="w-full" onClick={clearAll}>
{t`Clear all`}
</Button>
</div>
)}
</div>
<>
<Input
placeholder={t`Filter...`}
className="ps-4 pe-8 w-full sm:w-44"
onChange={handleChange}
value={containerFilter}
/>
{containerFilter && (
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Clear"
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
onClick={() => store.set("")}
>
<XIcon className="h-4 w-4" />
</Button>
)}
</div>
</>
)
}
@@ -1333,7 +1127,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}
@@ -1346,3 +1140,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>
)
}

View 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>
)
})

View File

@@ -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}

View 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>
)
}

View File

@@ -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} />

View File

@@ -1,8 +1,10 @@
import type { JSX } from "react"
import { useLingui } from "@lingui/react/macro"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { chartTimeData, cn } from "@/lib/utils"
import type { ChartData } from "@/types"
import { Separator } from "./separator"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
@@ -91,16 +93,18 @@ 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 | 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
showTotal?: boolean
totalLabel?: React.ReactNode
}
>(
(
{
@@ -121,27 +125,100 @@ const ChartTooltipContent = React.forwardRef<
itemSorter,
contentFormatter: content = undefined,
truncate = false,
showTotal = false,
totalLabel,
},
ref
) => {
// const { config } = useChart()
const config = {}
const { t } = useLingui()
const totalLabelNode = totalLabel ?? t`Total`
const totalName = typeof totalLabelNode === "string" ? totalLabelNode : t`Total`
React.useMemo(() => {
if (filter) {
if (Array.isArray(filter)) {
// Array filter: only show items that are in the filter array
payload = payload?.filter((item) => filter.includes(item.name as string))
} else {
// String filter: show items that match the string (backward compatibility)
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
payload?.sort(itemSorter)
}
}, [itemSorter, payload, filter])
}, [itemSorter, payload])
const totalValueDisplay = React.useMemo(() => {
if (!showTotal || !payload?.length) {
return null
}
let totalValue = 0
let hasNumericValue = false
const aggregatedNestedValues: Record<string, number> = {}
for (const item of payload) {
const numericValue = typeof item.value === "number" ? item.value : Number(item.value)
if (Number.isFinite(numericValue)) {
totalValue += numericValue
hasNumericValue = true
}
if (content && item?.payload) {
const payloadKey = `${nameKey || item.name || item.dataKey || "value"}`
const nestedPayload = (item.payload as Record<string, unknown> | undefined)?.[payloadKey]
if (nestedPayload && typeof nestedPayload === "object") {
for (const [nestedKey, nestedValue] of Object.entries(nestedPayload)) {
if (typeof nestedValue === "number" && Number.isFinite(nestedValue)) {
aggregatedNestedValues[nestedKey] = (aggregatedNestedValues[nestedKey] ?? 0) + nestedValue
}
}
}
}
}
if (!hasNumericValue) {
return null
}
const totalKey = "__total__"
const totalItem: any = {
value: totalValue,
name: totalName,
dataKey: totalKey,
color,
}
if (content) {
const basePayload =
payload[0]?.payload && typeof payload[0].payload === "object"
? { ...(payload[0].payload as Record<string, unknown>) }
: {}
totalItem.payload = {
...basePayload,
[totalKey]: aggregatedNestedValues,
}
}
if (typeof formatter === "function") {
return formatter(
totalValue,
totalName,
totalItem,
payload.length,
totalItem.payload ?? payload[0]?.payload
)
}
if (content) {
return content(totalItem, totalKey)
}
return `${totalValue.toLocaleString()}${unit ?? ""}`
}, [color, content, formatter, nameKey, payload, showTotal, totalName, unit])
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
@@ -244,6 +321,15 @@ const ChartTooltipContent = React.forwardRef<
</div>
)
})}
{totalValueDisplay ? (
<>
<Separator className="mt-0.5" />
<div className="flex items-center justify-between gap-2 -mt-0.75 font-medium">
<span className="text-muted-foreground ps-3">{totalLabelNode}</span>
<span>{totalValueDisplay}</span>
</div>
</>
) : null}
</div>
</div>
)
@@ -256,17 +342,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}
@@ -276,7 +365,7 @@ const ChartLegendContent = React.forwardRef<
className
)}
>
{payload.map((item) => {
{reversedPayload.map((item) => {
// const key = `${nameKey || item.dataKey || 'value'}`
// const itemConfig = getPayloadConfigFromPayload(config, item, key)

View File

@@ -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;
}
}

View File

@@ -12,11 +12,6 @@ export enum ChartType {
Disk,
Network,
CPU,
Volume,
Health,
Uptime,
HealthUptime,
DiskIO,
}
/** Unit of measurement */
@@ -59,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,

View File

@@ -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

View File

@@ -44,6 +44,11 @@ export default [
label: "Français",
e: "🇫🇷",
},
{
lang: "he",
label: "עברית",
e: "🕎",
},
{
lang: "hr",
label: "Hrvatski",

View 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'))

View File

@@ -53,13 +53,7 @@ export const $userSettings = map<UserSettings>({
listenKeys($userSettings, ["chartTime"], ({ chartTime }) => $chartTime.set(chartTime))
/** Container chart filter */
export const $containerFilter = atom<string[]>([])
/** Stack chart filter */
export const $stackFilter = atom<string[]>([])
/** Container color mapping for consistent colors across charts */
export const $containerColors = atom<Record<string, string>>({})
export const $containerFilter = atom("")
/** Temperature chart filter */
export const $temperatureFilter = atom("")

View File

@@ -1,4 +1,4 @@
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"
@@ -111,18 +111,17 @@ export const updateFavicon = (() => {
</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 &&
`
${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
; (document.querySelector("link[rel='icon']") as HTMLLinkElement).href = url
}
})()
@@ -288,46 +287,7 @@ export function formatBytes(
}
}
export const chartMargin = { top: 12 }
export function toFixedWithoutTrailingZeros(num: number, decimals: number): string {
const str = num.toFixed(decimals)
return str.replace(/\.?0+$/, "")
}
export const getSizeAndUnit = (n: number, isGigabytes = true) => {
const sizeInGB = isGigabytes ? n : n / 1_000
if (sizeInGB >= 1_000) {
return { v: sizeInGB / 1_000, u: " TB" }
}
if (sizeInGB >= 1) {
return { v: sizeInGB, u: " GB" }
}
return { v: isGigabytes ? sizeInGB * 1_000 : n, u: " MB" }
}
/**
* Generate a consistent fallback color for containers without assigned colors
* @param name Container name or identifier
* @returns HSL color string
*/
export function generateFallbackColor(name: string): string {
// Use a simple hash of the name to generate consistent colors
let hash = 0
for (let i = 0; i < name.length; i++) {
const char = name.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash // Convert to 32-bit integer
}
// Generate hue, saturation, and lightness from the hash
const hue = Math.abs(hash) % 360
const saturation = 65 + (Math.abs(hash) % 3) * 10 // 65%, 75%, 85%
const lightness = 50 + (Math.abs(hash) % 3) * 10 // 50%, 60%, 70%
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
}
export const chartMargin = { top: 12, right: 5 }
/**
* Retuns value of system host, truncating full path if socket.
@@ -406,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)
@@ -462,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` })
}
}

View File

@@ -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,9 +276,13 @@ msgstr "تحقق من السجلات لمزيد من التفاصيل."
msgid "Check your notification service"
msgstr "تحقق من خدمة الإشعارات الخاصة بك"
#: src/components/routes/system.tsx
msgid "Clear all"
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."
@@ -293,14 +306,10 @@ msgstr "هيئ التنبيهات الواردة"
msgid "Confirm password"
msgstr "تأكيد كلمة المرور"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "الاتصال مقطوع"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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 "استخدام وحدة المعالجة المركزية"
@@ -392,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 "لوحة التحكم"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "القرص"
msgid "Disk I/O"
msgstr "إدخال/إخراج القرص"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "وحدة القرص"
@@ -445,14 +472,6 @@ msgstr "استخدام القرص لـ {extraFsName}"
msgid "Docker CPU Usage"
msgstr "استخدام المعالج للدوكر"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "استخدام الذاكرة للدوكر"
@@ -461,14 +480,6 @@ msgstr "استخدام الذاكرة للدوكر"
msgid "Docker Network I/O"
msgstr "إدخال/إخراج الشبكة للدوكر"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "التوثيق"
@@ -536,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 {# دقائق}}"
@@ -560,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 "فشل في المصادقة"
@@ -577,15 +592,10 @@ msgstr "فشل في إرسال إشعار الاختبار"
msgid "Failed to update alert"
msgstr "فشل في تحديث التنبيه"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "تصفية..."
@@ -594,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 {دقائق}}"
@@ -631,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"
@@ -650,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 "عنوان البريد الإشباكي غير صالح."
@@ -702,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 "السجلات"
@@ -724,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 "الذاكرة"
@@ -737,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 "الشبكة"
@@ -762,18 +793,22 @@ msgstr "حركة مرور الشبكة للواجهات العامة"
msgid "Network unit"
msgstr "وحدة الشبكة"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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 "صفحة"
@@ -854,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> لضمان تسليم التنبيهات."
@@ -891,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"
@@ -915,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 "طلب كلمة مرور لمرة واحدة"
@@ -945,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 "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي."
@@ -974,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 "تعيين عتبات النسبة المئوية لألوان العداد."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "النظام"
msgid "System load averages over time"
msgstr "متوسط تحميل النظام مع مرور الوقت"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "الأنظمة"
@@ -1047,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 "درجة الحرارة"
@@ -1128,6 +1199,11 @@ msgstr "تسمح الرموز المميزة للوكلاء بالاتصال و
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "الإجمالي"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "إجمالي البيانات المستلمة لكل واجهة"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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 "رفع"
@@ -1209,6 +1293,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 "الاستخدام"
@@ -1234,6 +1319,7 @@ msgstr "القيمة"
msgid "View"
msgstr "عرض"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "عرض المزيد"
@@ -1246,10 +1332,6 @@ msgstr "عرض أحدث 200 تنبيه."
msgid "Visible Fields"
msgstr "الأعمدة الظاهرة"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "في انتظار وجود سجلات كافية للعرض"

View File

@@ -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,9 +276,13 @@ msgstr "Провери log-овете за повече информация."
msgid "Check your notification service"
msgstr "Провери услугата си за удостоверяване"
#: src/components/routes/system.tsx
msgid "Clear all"
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."
@@ -293,14 +306,10 @@ msgstr "Настрой как получаваш нотификации за т
msgid "Confirm password"
msgstr "Потвърди парола"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "Връзката е прекъсната"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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 "Употреба на процесор"
@@ -392,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 "Табло"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "Диск"
msgid "Disk I/O"
msgstr "Диск I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Единица за диск"
@@ -445,14 +472,6 @@ msgstr "Изполване на диск от {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Използване на процесор от docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Изполване на памет от docker"
@@ -461,14 +480,6 @@ msgstr "Изполване на памет от docker"
msgid "Docker Network I/O"
msgstr "Мрежов I/O използван от docker"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Документация"
@@ -536,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 {# минути}}"
@@ -560,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 "Неуспешно удостоверяване"
@@ -577,15 +592,10 @@ msgstr "Неуспешно изпрати тестова нотификация"
msgid "Failed to update alert"
msgstr "Неуспешно обнови тревога"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Филтрирай..."
@@ -594,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 {минути}}"
@@ -631,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"
@@ -650,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 "Невалиден имейл адрес."
@@ -702,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 "Логове"
@@ -724,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 "Памет"
@@ -737,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 "Мрежа"
@@ -762,18 +793,22 @@ msgstr "Мрежов трафик на публични интерфейси"
msgid "Network unit"
msgstr "Единица за измерване на скорост"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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 "Страница"
@@ -854,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> за да се подсигуриш, че тревогите са доставени."
@@ -891,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"
@@ -915,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 "Заявка за еднократна парола"
@@ -945,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 или запетая. Остави празно за да изключиш нотификациите чрез имейл."
@@ -974,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 "Задайте процентни прагове за цветовете на измервателните уреди."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "Система"
msgid "System load averages over time"
msgstr "Средно натоварване на системата във времето"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Системи"
@@ -1047,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 "Температура"
@@ -1128,6 +1199,11 @@ msgstr "Токените позволяват на агентите да се с
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токените и пръстовите отпечатъци се използват за удостоверяване на WebSocket връзките към концентратора."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Общо"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Общо получени данни за всеки интерфейс"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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 "Качване"
@@ -1209,6 +1293,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 "Употреба"
@@ -1234,6 +1319,7 @@ msgstr "Стойност"
msgid "View"
msgstr "Изглед"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Виж повече"
@@ -1246,10 +1332,6 @@ msgstr "Прегледайте последните си 200 сигнала."
msgid "Visible Fields"
msgstr "Видими полета"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Изчаква се за достатъчно записи за показване"

View File

@@ -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,9 +276,13 @@ msgstr "Pro více informací zkontrolujte logy."
msgid "Check your notification service"
msgstr "Zkontrolujte službu upozornění"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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."
@@ -293,14 +306,10 @@ 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é"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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"
@@ -392,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"
@@ -410,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"
@@ -417,14 +448,10 @@ 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"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
@@ -445,14 +472,6 @@ msgstr "Využití disku {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Využití CPU Dockeru"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Využití paměti Dockeru"
@@ -461,14 +480,6 @@ msgstr "Využití paměti Dockeru"
msgid "Docker Network I/O"
msgstr "Síťové I/O Dockeru"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentace"
@@ -502,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"
@@ -536,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}}"
@@ -546,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"
@@ -560,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"
@@ -577,15 +592,10 @@ msgstr "Nepodařilo se odeslat testovací oznámení"
msgid "Failed to update alert"
msgstr "Nepodařilo se aktualizovat upozornění"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filtr..."
@@ -594,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}}"
@@ -631,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"
@@ -650,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."
@@ -657,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"
@@ -702,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"
@@ -724,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ěť"
@@ -737,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íť"
@@ -762,18 +793,22 @@ msgstr "Síťový provoz veřejných rozhraní"
msgid "Network unit"
msgstr "Síťová jednotka"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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"
@@ -854,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."
@@ -889,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
@@ -915,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"
@@ -945,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."
@@ -974,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čů."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "Systém"
msgid "System load averages over time"
msgstr "Průměry zatížení systému v průběhu času"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systémy"
@@ -1047,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"
@@ -1066,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"
@@ -1112,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
@@ -1128,6 +1199,11 @@ msgstr "Tokeny umožňují agentům připojení a registraci. Otisky jsou stabil
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny a otisky slouží k ověření připojení WebSocket k uzlu."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Celkem"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Celkový přijatý objem dat pro každé rozhraní"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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í"
@@ -1209,6 +1293,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í"
@@ -1234,6 +1319,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"
@@ -1246,10 +1332,6 @@ msgstr "Zobrazit vašich 200 nejnovějších upozornění."
msgid "Visible Fields"
msgstr "Viditelné sloupce"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Čeká se na dostatek záznamů k zobrazení"

View File

@@ -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,13 +276,17 @@ msgstr "Tjek logfiler for flere detaljer."
msgid "Check your notification service"
msgstr "Tjek din notifikationstjeneste"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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"
@@ -293,13 +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 ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
msgstr "Forbindelsen er nede"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -325,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"
@@ -346,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"
@@ -373,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 (%)"
@@ -392,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"
@@ -408,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
@@ -423,13 +454,9 @@ msgstr "Disk"
msgid "Disk I/O"
msgstr "Disk I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr ""
msgstr "Diskenhed"
#: src/components/charts/disk-chart.tsx
#: src/components/routes/system.tsx
@@ -445,14 +472,6 @@ msgstr "Diskforbrug af {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU forbrug"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker Hukommelsesforbrug"
@@ -461,14 +480,6 @@ msgstr "Docker Hukommelsesforbrug"
msgid "Docker Network I/O"
msgstr "Docker Netværk I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentation"
@@ -483,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
@@ -536,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}}"
@@ -546,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"
@@ -558,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"
@@ -577,21 +592,20 @@ msgstr "Afsendelse af testnotifikation mislykkedes"
msgid "Failed to update alert"
msgstr "Kunne ikke opdatere alarm"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "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
@@ -621,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"
@@ -631,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"
@@ -650,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."
@@ -657,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"
@@ -665,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"
@@ -702,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"
@@ -724,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"
@@ -737,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"
@@ -760,19 +791,23 @@ msgstr "Netværkstrafik af offentlige grænseflader"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr ""
#: src/components/routes/system.tsx
msgid "No items available"
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
@@ -807,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"
@@ -819,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"
@@ -852,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."
@@ -891,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"
@@ -915,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"
@@ -931,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"
@@ -939,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."
@@ -974,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."
@@ -1004,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"
@@ -1020,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
@@ -1028,11 +1102,7 @@ msgstr "System"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
msgstr "Gennemsnitlig system belastning over tid"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1047,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"
@@ -1058,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"
@@ -1082,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}"
@@ -1112,21 +1183,26 @@ 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/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Samlet"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
@@ -1138,15 +1214,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"
@@ -1172,15 +1248,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
@@ -1195,11 +1275,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"
@@ -1209,6 +1293,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"
@@ -1228,28 +1313,25 @@ 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"
msgstr "Synlige felter"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Venter på nok posteringer til at vise"
@@ -1272,7 +1354,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

View File

@@ -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,9 +276,13 @@ msgstr "Überprüfe die Protokolle für weitere Details."
msgid "Check your notification service"
msgstr "Überprüfe deinen Benachrichtigungsdienst"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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."
@@ -293,14 +306,10 @@ 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"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -346,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"
@@ -392,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"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "Festplatte"
msgid "Disk I/O"
msgstr "Festplatten-I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Festplatteneinheit"
@@ -445,14 +472,6 @@ msgstr "Festplattennutzung von {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker-CPU-Auslastung"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker-Arbeitsspeichernutzung"
@@ -461,14 +480,6 @@ msgstr "Docker-Arbeitsspeichernutzung"
msgid "Docker Network I/O"
msgstr "Docker-Netzwerk-I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentation"
@@ -536,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}}"
@@ -560,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"
@@ -577,15 +592,10 @@ msgstr "Testbenachrichtigung konnte nicht gesendet werden"
msgid "Failed to update alert"
msgstr "Warnung konnte nicht aktualisiert werden"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filter..."
@@ -594,6 +604,10 @@ msgstr "Filter..."
msgid "Fingerprint"
msgstr "Fingerabdruck"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr "Firm­ware"
#: 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}}"
@@ -631,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"
@@ -650,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."
@@ -702,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"
@@ -724,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"
@@ -737,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"
@@ -762,18 +793,22 @@ msgstr "Netzwerkverkehr der öffentlichen Schnittstellen"
msgid "Network unit"
msgstr "Netzwerkeinheit"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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"
@@ -854,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."
@@ -891,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"
@@ -903,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
@@ -915,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"
@@ -945,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."
@@ -956,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"
@@ -974,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."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "System"
msgid "System load averages over time"
msgstr "Systemlastdurchschnitt im Zeitverlauf"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systeme"
@@ -1047,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"
@@ -1128,13 +1199,18 @@ msgstr "Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fi
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Gesamt"
#: 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"
@@ -1154,7 +1230,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"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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"
@@ -1209,6 +1293,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"
@@ -1234,6 +1319,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"
@@ -1246,10 +1332,6 @@ msgstr "Sieh dir die neusten 200 Alarme an."
msgid "Visible Fields"
msgstr "Sichtbare Spalten"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Warten auf genügend Datensätze zur Anzeige"

File diff suppressed because it is too large Load Diff

View File

@@ -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,9 +271,13 @@ msgstr "Check logs for more details."
msgid "Check your notification service"
msgstr "Check your notification service"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr "Clear all"
#: 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."
@@ -288,14 +301,10 @@ 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"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr "Container health status and uptime"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -351,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"
@@ -387,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"
@@ -405,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"
@@ -418,10 +449,6 @@ msgstr "Disk"
msgid "Disk I/O"
msgstr "Disk I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr "Disk read/write rates of docker containers"
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Disk unit"
@@ -440,14 +467,6 @@ msgstr "Disk usage of {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU Usage"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr "Docker Disk I/O"
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr "Docker Health & Uptime"
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker Memory Usage"
@@ -456,14 +475,6 @@ msgstr "Docker Memory Usage"
msgid "Docker Network I/O"
msgstr "Docker Network I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr "Docker Stats"
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr "Docker Volumes"
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Documentation"
@@ -531,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}}"
@@ -555,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"
@@ -572,15 +587,10 @@ msgstr "Failed to send test notification"
msgid "Failed to update alert"
msgstr "Failed to update alert"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr "Filter containers..."
#: src/components/routes/system.tsx
msgid "Filter stacks..."
msgstr "Filter stacks..."
#: 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..."
@@ -589,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}}"
@@ -626,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"
@@ -645,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."
@@ -697,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"
@@ -719,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"
@@ -732,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"
@@ -757,18 +788,22 @@ msgstr "Network traffic of public interfaces"
msgid "Network unit"
msgstr "Network unit"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr "No items available"
#: src/components/command-palette.tsx
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."
@@ -802,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"
@@ -849,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."
@@ -886,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"
@@ -910,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"
@@ -940,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."
@@ -969,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."
@@ -1001,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"
@@ -1015,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
@@ -1025,10 +1099,6 @@ msgstr "System"
msgid "System load averages over time"
msgstr "System load averages over time"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr "System Stats"
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systems"
@@ -1042,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"
@@ -1123,6 +1194,11 @@ msgstr "Tokens allow agents to connect and register. Fingerprints are stable ide
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Total data received for each interface"
@@ -1167,6 +1243,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"
@@ -1192,6 +1272,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"
@@ -1204,6 +1288,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"
@@ -1229,6 +1314,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"
@@ -1241,10 +1327,6 @@ msgstr "View your 200 most recent alerts."
msgid "Visible Fields"
msgstr "Visible Fields"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr "Volume usage of docker containers"
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Waiting for enough records to display"

View File

@@ -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-04 22:13\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"
@@ -126,22 +123,30 @@ msgstr "Agente"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx
msgid "Alert History"
msgstr "Historial de Alertas"
msgstr "Historial de alertas"
#: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx
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"
msgstr "Todos los Sistemas"
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,31 +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/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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
@@ -286,21 +299,17 @@ 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"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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"
@@ -373,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 (%)"
@@ -381,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
@@ -410,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,10 +454,6 @@ msgstr "Disco"
msgid "Disk I/O"
msgstr "E/S de Disco"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Unidad de disco"
@@ -435,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}"
@@ -445,29 +472,13 @@ msgstr "Uso de disco de {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Uso de CPU de Docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: 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"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
msgstr "E/S de red de Docker"
#: src/components/command-palette.tsx
msgid "Documentation"
@@ -479,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"
@@ -515,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."
@@ -536,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"
@@ -554,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"
@@ -577,15 +592,10 @@ msgstr "Error al enviar la notificación de prueba"
msgid "Failed to update alert"
msgstr "Error al actualizar la alerta"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filtrar..."
@@ -594,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
@@ -631,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"
@@ -639,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
@@ -648,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."
@@ -669,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"
@@ -690,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"
@@ -702,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."
@@ -722,8 +746,9 @@ msgstr "Instrucciones manuales de configuración"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
msgid "Max 1 min"
msgstr "Máx 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"
@@ -731,24 +756,30 @@ msgstr "Memoria"
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage"
msgstr "Uso de Memoria"
msgstr "Uso de memoria"
#: src/components/routes/system.tsx
msgid "Memory usage of docker containers"
msgstr "Uso de memoria de los contenedores de Docker"
msgstr "Uso de memoria de los contenedores 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"
#: src/components/routes/system.tsx
msgid "Network traffic of docker containers"
msgstr "Tráfico de red de los contenedores de Docker"
msgstr "Tráfico de red de los contenedores Docker"
#: src/components/routes/system.tsx
#: src/components/routes/system/network-sheet.tsx
@@ -762,18 +793,22 @@ msgstr "Tráfico de red de interfaces públicas"
msgid "Network unit"
msgstr "Unidad de red"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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"
@@ -854,43 +894,57 @@ 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 "Uso 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."
msgstr "Por favor, <0>configura un servidor SMTP</0> para asegurar que las alertas sean entregadas."
#: src/components/alerts/alerts-sheet.tsx
msgid "Please check logs for more details."
msgstr "Por favor, revise los registros para más detalles."
msgstr "Por favor, revisa los registros para más detalles."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
msgid "Please check your credentials and try again"
msgstr "Por favor, verifique sus credenciales e intente de nuevo"
msgstr "Por favor, verifica tus credenciales e inténtalo de nuevo"
#: src/components/login/login.tsx
msgid "Please create an admin account"
msgstr "Por favor, cree una cuenta de administrador"
msgstr "Por favor, crea una cuenta de administrador"
#: src/components/login/auth-form.tsx
msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite las ventanas emergentes para este sitio"
msgstr "Por favor, habilita las ventanas emergentes para este sitio"
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Por favor, inicie sesión de nuevo"
msgstr "Por favor, inicia sesión de nuevo"
#: src/components/login/auth-form.tsx
msgid "Please see <0>the documentation</0> for instructions."
msgstr "Por favor, consulte <0>la documentación</0> para obtener instrucciones."
msgstr "Por favor, consulta <0>la documentación</0> para obtener instrucciones."
#: src/components/login/login.tsx
msgid "Please sign in to your account"
msgstr "Por favor, inicie sesión en su cuenta"
msgstr "Por favor, inicia sesión en tu cuenta"
#: src/components/add-system.tsx
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"
@@ -898,12 +952,12 @@ msgstr "Utilización precisa en el momento registrado"
#: src/components/routes/settings/general.tsx
msgid "Preferred Language"
msgstr "Idioma Preferido"
msgstr "Idioma preferido"
#. Use 'Key' if your language requires many more characters
#: src/components/add-system.tsx
msgid "Public Key"
msgstr "Clave Pública"
msgstr "Clave pública"
#. Disk read
#: src/components/routes/system.tsx
@@ -915,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"
@@ -925,7 +984,7 @@ msgstr "Solicitar OTP"
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Restablecer Contraseña"
msgstr "Restablecer contraseña"
#: src/components/alerts-history-columns.tsx
#: src/components/alerts-history-columns.tsx
@@ -945,18 +1004,26 @@ 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."
msgstr "Guarda la dirección usando la tecla enter o coma. Deja en blanco para desactivar las notificaciones por correo."
#: 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"
@@ -968,12 +1035,16 @@ msgstr "Buscar sistemas o configuraciones..."
#: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Consulte <0>configuración de notificaciones</0> para configurar cómo recibe alertas."
msgstr "Consulta <0>configuración de notificaciones</0> para configurar cómo recibe alertas."
#: src/components/routes/system.tsx
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."
@@ -1006,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"
@@ -1017,9 +1090,10 @@ msgstr "Espacio de swap utilizado por el sistema"
#: src/components/routes/system.tsx
msgid "Swap Usage"
msgstr "Uso de Swap"
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
@@ -1030,23 +1104,20 @@ msgstr "Sistema"
msgid "System load averages over time"
msgstr "Promedios de carga del sistema a lo largo del tiempo"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Sistemas"
#: src/components/routes/settings/config-yaml.tsx
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
msgstr "Los sistemas pueden ser gestionados en un archivo <0>config.yml</0> dentro de su directorio de datos."
msgstr "Los sistemas pueden ser gestionados en un archivo <0>config.yml</0> dentro de tu directorio de datos."
#: src/components/systems-table/systems-table.tsx
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"
@@ -1074,7 +1145,7 @@ msgstr "Notificación de prueba enviada"
#: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Luego inicie sesión en el backend y restablezca la contraseña de su cuenta de usuario en la tabla de usuarios."
msgstr "Luego inicia sesión en el backend y restablece la contraseña de tu cuenta de usuario en la tabla de usuarios."
#: src/components/systems-table/systems-table-columns.tsx
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
@@ -1118,7 +1189,7 @@ msgstr "Token"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens & Fingerprints"
msgstr "Tokens y Huellas Digitales"
msgstr "Tokens y huellas digitales"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
@@ -1128,6 +1199,11 @@ msgstr "Los tokens permiten que los agentes se conecten y registren. Las huellas
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Los tokens y las huellas digitales se utilizan para autenticar las conexiones WebSocket al hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Datos totales recibidos por cada interfaz"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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"
@@ -1209,6 +1293,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"
@@ -1234,29 +1319,26 @@ 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"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr "Ver sus 200 alertas más recientes."
msgstr "Ver tus 200 alertas más recientes."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Columnas visibles"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Esperando suficientes registros para mostrar"
#: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "¿Quieres ayudarnos a mejorar nuestras traducciones? Consulta <0>Crowdin</0> para más detalles."
msgstr "¿Quieres ayudar a mejorar nuestras traducciones? Consulta <0>Crowdin</0> para más detalles."
#: src/components/routes/settings/general.tsx
msgid "Warning (%)"
@@ -1296,4 +1378,4 @@ msgstr "Configuración YAML"
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Su configuración de usuario ha sido actualizada."
msgstr "Tu configuración de usuario ha sido actualizada."

View File

@@ -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,9 +276,13 @@ msgstr "برای جزئیات بیشتر، لاگ‌ها را بررسی کنی
msgid "Check your notification service"
msgstr "سرویس اطلاع‌رسانی خود را بررسی کنید"
#: src/components/routes/system.tsx
msgid "Clear all"
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."
@@ -293,14 +306,10 @@ msgstr "نحوه دریافت هشدارهای اطلاع‌رسانی را پی
msgid "Confirm password"
msgstr "تأیید رمز عبور"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "اتصال قطع است"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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 "میزان استفاده از پردازنده"
@@ -392,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 "داشبورد"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "دیسک"
msgid "Disk I/O"
msgstr "ورودی/خروجی دیسک"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "واحد دیسک"
@@ -445,14 +472,6 @@ msgstr "میزان استفاده از دیسک {extraFsName}"
msgid "Docker CPU Usage"
msgstr "میزان استفاده از CPU داکر"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "میزان استفاده از حافظه داکر"
@@ -461,14 +480,6 @@ msgstr "میزان استفاده از حافظه داکر"
msgid "Docker Network I/O"
msgstr "ورودی/خروجی شبکه داکر"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "مستندات"
@@ -536,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} بیشتر است"
@@ -560,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 "احراز هویت ناموفق بود"
@@ -577,15 +592,10 @@ msgstr "ارسال اعلان آزمایشی ناموفق بود"
msgid "Failed to update alert"
msgstr "به‌روزرسانی هشدار ناموفق بود"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "فیلتر..."
@@ -594,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 {دقیقه}}"
@@ -631,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"
@@ -650,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 "آدرس ایمیل نامعتبر است."
@@ -702,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 "لاگ‌ها"
@@ -724,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 "حافظه"
@@ -737,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 "شبکه"
@@ -762,18 +793,22 @@ msgstr "ترافیک شبکه رابط‌های عمومی"
msgid "Network unit"
msgstr "واحد شبکه"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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 "صفحه"
@@ -854,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>."
@@ -891,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"
@@ -915,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 "درخواست رمز عبور یک‌بار مصرف"
@@ -945,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 یا کاما ذخیره کنید. برای غیرفعال کردن اعلان‌های ایمیلی، خالی بگذارید."
@@ -974,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 "آستانه های درصدی را برای رنگ های متر تنظیم کنید."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "سیستم"
msgid "System load averages over time"
msgstr "میانگین بار سیستم در طول زمان"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "سیستم‌ها"
@@ -1047,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 "دما"
@@ -1128,6 +1199,11 @@ msgstr "توکن‌ها به عامل‌ها اجازه اتصال و ثبت‌
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "توکن‌ها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده می‌شوند."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "کل"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "داده‌های کل دریافت شده برای هر رابط"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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 "آپلود"
@@ -1209,6 +1293,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 "میزان استفاده"
@@ -1234,6 +1319,7 @@ msgstr "مقدار"
msgid "View"
msgstr "مشاهده"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "مشاهده بیشتر"
@@ -1246,10 +1332,6 @@ msgstr "۲۰۰ هشدار اخیر خود را مشاهده کنید."
msgid "Visible Fields"
msgstr "فیلدهای قابل مشاهده"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "در انتظار رکوردهای کافی برای نمایش"

View File

@@ -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,13 +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/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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"
@@ -293,13 +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 ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
msgstr "Connexion interrompue"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -356,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"
@@ -373,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 (%)"
@@ -392,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"
@@ -410,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"
@@ -423,13 +454,9 @@ msgstr "Disque"
msgid "Disk I/O"
msgstr "Entrée/Sortie disque"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr ""
msgstr "Unité disque"
#: src/components/charts/disk-chart.tsx
#: src/components/routes/system.tsx
@@ -445,14 +472,6 @@ msgstr "Utilisation du disque de {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Utilisation du CPU Docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Utilisation de la mémoire Docker"
@@ -461,14 +480,6 @@ msgstr "Utilisation de la mémoire Docker"
msgid "Docker Network I/O"
msgstr "Entrée/Sortie réseau Docker"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Documentation"
@@ -483,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"
@@ -491,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
@@ -536,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}}"
@@ -546,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"
@@ -558,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"
@@ -577,22 +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/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "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}}"
@@ -631,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"
@@ -650,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."
@@ -669,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"
@@ -702,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"
@@ -724,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"
@@ -737,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"
@@ -760,19 +791,23 @@ msgstr "Trafic réseau des interfaces publiques"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr ""
#: src/components/routes/system.tsx
msgid "No items available"
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
@@ -807,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"
@@ -819,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"
@@ -852,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."
@@ -891,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"
@@ -915,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"
@@ -931,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"
@@ -943,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."
@@ -974,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."
@@ -1004,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"
@@ -1020,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
@@ -1028,11 +1102,7 @@ msgstr "Système"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
msgstr "Charges moyennes du système dans le temps"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1047,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."
@@ -1058,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"
@@ -1082,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}"
@@ -1112,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
@@ -1128,6 +1199,11 @@ msgstr "Les tokens permettent aux agents de se connecter et de s'enregistrer. Le
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Les tokens et les empreintes sont utilisés pour authentifier les connexions WebSocket vers le hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Données totales reçues pour chaque interface"
@@ -1138,15 +1214,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"
@@ -1172,10 +1248,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
@@ -1195,7 +1275,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"
@@ -1209,6 +1293,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"
@@ -1228,28 +1313,25 @@ 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"
msgstr "Colonnes visibles"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "En attente de suffisamment d'enregistrements à afficher"

File diff suppressed because it is too large Load Diff

View File

@@ -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,13 +276,17 @@ msgstr "Provjerite logove za više detalja."
msgid "Check your notification service"
msgstr "Provjerite Vaš servis notifikacija"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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"
@@ -293,13 +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 ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
msgstr "Veza je pala"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -346,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"
@@ -392,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"
@@ -408,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
@@ -423,13 +454,9 @@ msgstr "Disk"
msgid "Disk I/O"
msgstr "Disk I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: 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
@@ -445,14 +472,6 @@ msgstr "Iskorištenost diska od {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Iskorištenost Docker Procesora"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Iskorištenost Docker Memorije"
@@ -461,14 +480,6 @@ msgstr "Iskorištenost Docker Memorije"
msgid "Docker Network I/O"
msgstr "Docker Mrežni I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentacija"
@@ -479,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"
@@ -536,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}}"
@@ -558,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"
@@ -577,15 +592,10 @@ msgstr "Neuspješno slanje testne notifikacije"
msgid "Failed to update alert"
msgstr "Ažuriranje upozorenja nije uspjelo"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filtriraj..."
@@ -594,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}}"
@@ -625,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"
@@ -650,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."
@@ -657,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"
@@ -669,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
@@ -702,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"
@@ -717,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"
@@ -737,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"
@@ -760,20 +791,24 @@ msgstr "Mrežni promet javnih sučelja"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr ""
#: src/components/routes/system.tsx
msgid "No items available"
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."
@@ -807,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"
@@ -854,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."
@@ -891,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"
@@ -915,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"
@@ -931,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"
@@ -939,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."
@@ -956,7 +1023,7 @@ msgstr "Spremi Postavke"
#: src/components/add-system.tsx
msgid "Save system"
msgstr ""
msgstr "Spremi sustav"
#: src/components/navbar.tsx
msgid "Search"
@@ -974,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."
@@ -1004,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"
@@ -1020,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
@@ -1028,11 +1102,7 @@ msgstr "Sistem"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
msgstr "Prosječno opterećenje sustava kroz vrijeme"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1047,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
@@ -1058,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"
@@ -1082,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}"
@@ -1112,21 +1183,26 @@ 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/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Ukupno"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
@@ -1138,15 +1214,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"
@@ -1172,15 +1248,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
@@ -1191,11 +1271,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"
@@ -1209,6 +1293,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"
@@ -1234,22 +1319,19 @@ 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"
msgstr "Vidljiva polja"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Čeka se na više podataka prije prikaza"
@@ -1272,7 +1354,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

View File

@@ -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,13 +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/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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"
@@ -293,13 +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 ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
msgstr "Kapcsolat megszakadt"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -325,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"
@@ -354,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"
@@ -373,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 (%)"
@@ -392,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"
@@ -408,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
@@ -423,13 +454,9 @@ msgstr "Lemez"
msgid "Disk I/O"
msgstr "Lemez I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: 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
@@ -445,14 +472,6 @@ msgstr "Lemezhasználat a {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU használat"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker memória használat"
@@ -461,14 +480,6 @@ msgstr "Docker memória használat"
msgid "Docker Network I/O"
msgstr "Docker hálózat I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentáció"
@@ -479,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"
@@ -491,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
@@ -536,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}}"
@@ -546,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"
@@ -558,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"
@@ -577,21 +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/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "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
@@ -621,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"
@@ -631,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"
@@ -650,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."
@@ -669,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"
@@ -702,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"
@@ -717,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"
@@ -737,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"
@@ -760,19 +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 ""
#: src/components/routes/system.tsx
msgid "No items available"
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
@@ -807,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"
@@ -819,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"
@@ -836,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"
@@ -852,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."
@@ -891,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"
@@ -915,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"
@@ -931,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"
@@ -939,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."
@@ -956,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"
@@ -974,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."
@@ -1004,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"
@@ -1020,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
@@ -1028,11 +1102,7 @@ msgstr "Rendszer"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
msgstr "Rendszer terhelési átlaga"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1047,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
@@ -1058,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"
@@ -1082,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}"
@@ -1112,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."
@@ -1128,6 +1199,11 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Összesen"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Összes fogadott adat minden interfészenként"
@@ -1138,15 +1214,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"
@@ -1172,15 +1248,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
@@ -1191,11 +1271,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"
@@ -1209,6 +1293,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"
@@ -1228,28 +1313,25 @@ 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"
msgstr "Látható mezők"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Elegendő rekordra várva a megjelenítéshez"

File diff suppressed because it is too large Load Diff

View File

@@ -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,8 +279,12 @@ 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/routes/system.tsx
msgid "Clear all"
#: 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
@@ -293,14 +309,10 @@ 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 ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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"
@@ -392,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"
@@ -410,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"
@@ -423,10 +448,6 @@ msgstr "Diskur"
msgid "Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr ""
@@ -445,14 +466,6 @@ msgstr "Diska notkun af {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU notkun"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Minnisnotkun Docker"
@@ -461,14 +474,6 @@ msgstr "Minnisnotkun Docker"
msgid "Docker Network I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Skjal"
@@ -536,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}}"
@@ -560,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"
@@ -577,15 +586,10 @@ msgstr "Villa í sendingu prufu skilaboða"
msgid "Failed to update alert"
msgstr "Mistókst að uppfæra tilkynningu"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Sía..."
@@ -594,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 ""
@@ -611,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
@@ -631,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"
@@ -639,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
@@ -650,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."
@@ -702,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"
@@ -724,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"
@@ -737,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"
@@ -753,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 ""
@@ -762,18 +786,22 @@ msgstr ""
msgid "Network unit"
msgstr ""
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -811,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"
@@ -889,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
@@ -945,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 ""
@@ -972,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."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1070,6 @@ msgstr "Kerfi"
msgid "System load averages over time"
msgstr ""
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Kerfi"
@@ -1047,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 ""
@@ -1172,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"
@@ -1246,10 +1287,6 @@ msgstr ""
msgid "Visible Fields"
msgstr "Sjáanlegir reitir"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Bíður eftir nægum upplýsingum til að sýna"
@@ -1297,3 +1334,4 @@ msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Notenda stillingar vistaðar."

View File

@@ -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,9 +276,13 @@ msgstr "Controlla i log per maggiori dettagli."
msgid "Check your notification service"
msgstr "Controlla il tuo servizio di notifica"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: 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."
@@ -293,14 +306,10 @@ 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"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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"
@@ -392,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"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "Disco"
msgid "Disk I/O"
msgstr "I/O Disco"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Unità disco"
@@ -445,14 +472,6 @@ msgstr "Utilizzo del disco di {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Utilizzo CPU Docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Utilizzo Memoria Docker"
@@ -461,14 +480,6 @@ msgstr "Utilizzo Memoria Docker"
msgid "Docker Network I/O"
msgstr "I/O di Rete Docker"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Documentazione"
@@ -536,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}}"
@@ -560,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"
@@ -577,15 +592,10 @@ msgstr "Invio della notifica di test fallito"
msgid "Failed to update alert"
msgstr "Aggiornamento dell'avviso fallito"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filtra..."
@@ -594,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}}"
@@ -631,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"
@@ -650,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."
@@ -702,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"
@@ -724,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"
@@ -737,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"
@@ -762,18 +793,22 @@ msgstr "Traffico di rete delle interfacce pubbliche"
msgid "Network unit"
msgstr "Unità rete"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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"
@@ -854,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."
@@ -891,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"
@@ -915,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"
@@ -945,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."
@@ -974,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."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "Sistema"
msgid "System load averages over time"
msgstr "Medie di carico del sistema nel tempo"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Sistemi"
@@ -1047,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"
@@ -1128,6 +1199,11 @@ msgstr "I token consentono agli agenti di connettersi e registrarsi. Le impronte
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "I token e le impronte digitali vengono utilizzati per autenticare le connessioni WebSocket all'hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Totale"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Dati totali ricevuti per ogni interfaccia"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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"
@@ -1209,6 +1293,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"
@@ -1234,6 +1319,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"
@@ -1246,10 +1332,6 @@ msgstr "Visualizza i tuoi 200 avvisi più recenti."
msgid "Visible Fields"
msgstr "Colonne visibili"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "In attesa di abbastanza record da visualizzare"

View File

@@ -8,30 +8,15 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: ja\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 {# 分} other {# 分}}"
msgid "{0} of {1} row(s) selected."
msgstr "{1}行のうち{0}行が選択されました。"
#: 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,9 +276,13 @@ msgstr "詳細についてはログを確認してください。"
msgid "Check your notification service"
msgstr "通知サービスを確認してください"
#: src/components/routes/system.tsx
msgid "Clear all"
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."
@@ -293,14 +306,10 @@ msgstr "アラート通知の受信方法を設定します。"
msgid "Confirm password"
msgstr "パスワードを確認"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "接続が切断されました"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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 "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 "CPU使用率"
@@ -392,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 "ダッシュボード"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "ディスク"
msgid "Disk I/O"
msgstr "ディスクI/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "ディスク単位"
@@ -445,14 +472,6 @@ msgstr "{extraFsName}のディスク使用率"
msgid "Docker CPU Usage"
msgstr "Docker CPU使用率"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Dockerメモリ使用率"
@@ -461,14 +480,6 @@ msgstr "Dockerメモリ使用率"
msgid "Docker Network I/O"
msgstr "DockerネットワークI/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "ドキュメント"
@@ -536,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}を超えています"
@@ -560,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 "認証に失敗しました"
@@ -577,15 +592,10 @@ msgstr "テスト通知の送信に失敗しました"
msgid "Failed to update alert"
msgstr "アラートの更新に失敗しました"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "フィルター..."
@@ -594,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 {分}}の間"
@@ -631,6 +645,10 @@ msgstr "GPUの消費電力"
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"
@@ -650,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 "無効なメールアドレスです。"
@@ -702,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 "ログ"
@@ -724,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 "メモリ"
@@ -737,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 "帯域"
@@ -762,18 +793,22 @@ msgstr "パブリックインターフェースのネットワークトラフィ
msgid "Network unit"
msgstr "ネットワーク単位"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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 "ページ"
@@ -854,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>してください。"
@@ -891,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"
@@ -915,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 "ワンタイムパスワードをリクエスト"
@@ -945,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キーまたはカンマを使用してアドレスを保存します。空白のままにするとメール通知が無効になります。"
@@ -974,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 "メーターの色を変更するしきい値(パーセンテージ)を設定します。"
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "システム"
msgid "System load averages over time"
msgstr "システムの負荷平均の推移"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "システム"
@@ -1047,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 "温度"
@@ -1128,6 +1199,11 @@ msgstr "トークンはエージェントの接続と登録を可能にします
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。"
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "総数"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "各インターフェースの総受信データ量"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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 "アップロード"
@@ -1209,6 +1293,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 "使用量"
@@ -1234,6 +1319,7 @@ msgstr "値"
msgid "View"
msgstr "表示"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "もっと見る"
@@ -1246,10 +1332,6 @@ msgstr "直近200件のアラートを表示します。"
msgid "Visible Fields"
msgstr "表示列"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "表示するのに十分なレコードを待っています"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-23 02:45\n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -18,27 +18,24 @@ 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 {# 일} other {# 일}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# 시간} other {# 시간}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# 분} few {# 분} many {# 분} other {# 분}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{1}개의 행 중 {0}개가 선택되었습니다."
#: src/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,9 +276,13 @@ msgstr "자세한 내용은 로그를 확인하세요."
msgid "Check your notification service"
msgstr "알림 서비스를 확인하세요."
#: src/components/routes/system.tsx
msgid "Clear all"
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."
@@ -293,14 +306,10 @@ msgstr "알림을 수신할 방법을 설정하세요."
msgid "Confirm password"
msgstr "비밀번호 확인"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "연결이 끊겼습니다"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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 "CPU"
#: 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 "CPU 사용량"
@@ -392,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 "대시보드"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "디스크"
msgid "Disk I/O"
msgstr "디스크 I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "디스크 단위"
@@ -445,14 +472,6 @@ msgstr "{extraFsName}의 디스크 사용량"
msgid "Docker CPU Usage"
msgstr "Docker CPU 사용량"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker 메모리 사용량"
@@ -461,14 +480,6 @@ msgstr "Docker 메모리 사용량"
msgid "Docker Network I/O"
msgstr "Docker 네트워크 I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "문서"
@@ -536,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} 초과"
@@ -560,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 "인증 실패"
@@ -577,15 +592,10 @@ msgstr "테스트 알림 전송 실패"
msgid "Failed to update alert"
msgstr "알림 수정 실패"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "필터..."
@@ -594,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 {분}} 동안"
@@ -631,6 +645,10 @@ msgstr "GPU 전원 사용량"
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"
@@ -650,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 "잘못된 이메일 주소입니다."
@@ -702,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 "로그"
@@ -724,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 "메모리"
@@ -737,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 "네트워크"
@@ -762,18 +793,22 @@ msgstr "공용 인터페이스의 네트워크 트래픽"
msgid "Network unit"
msgstr "네트워크 단위"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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 "페이지"
@@ -854,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>하세요."
@@ -891,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"
@@ -915,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 "OTP 요청"
@@ -945,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 키 또는 쉼표를 사용하여 주소를 저장하세요. 이메일 알림을 비활성화하려면 비워 두세요."
@@ -974,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 "그래프 미터 색상의 백분율 임계값을 설정합니다."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "시스템"
msgid "System load averages over time"
msgstr "시간에 따른 시스템 부하 평균"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "시스템"
@@ -1047,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 "온도"
@@ -1128,6 +1199,11 @@ msgstr "토큰은 에이전트가 연결하고 등록할 수 있도록 합니다
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "토큰과 지문은 허브에 대한 WebSocket 연결을 인증하는 데 사용됩니다."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "총"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "각 인터페이스별 총합 다운로드 데이터량"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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 "업로드"
@@ -1209,6 +1293,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 "사용량"
@@ -1234,6 +1319,7 @@ msgstr "값"
msgid "View"
msgstr "보기"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "더 보기"
@@ -1246,10 +1332,6 @@ msgstr "최근 200개의 알림을 봅니다."
msgid "Visible Fields"
msgstr "표시할 열"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "표시할 충분한 기록을 기다리는 중"

View File

@@ -8,30 +8,15 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"PO-Revision-Date: 2025-10-28 22:59\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: nl\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 {# dag} other {# dagen}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# uur} other {# uren}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuut} other {# minuten}}"
"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 {# minuut} other {# minuten}}"
msgid "{0} of {1} row(s) selected."
msgstr "{0} van de {1} rij(en) geselecteerd."
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
msgstr "{count, plural, one {{countString} dag} other {{countString} dagen}}"
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
msgstr "{count, plural, one {{countString} uur} other {{countString} uren}}"
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
msgstr "{count, plural, one {{countString} minuut} other {{countString} minuten}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 uur"
@@ -54,7 +51,7 @@ msgstr "1 minuut"
#: src/lib/utils.ts
msgid "1 week"
msgstr "1 week"
msgstr ""
#: src/lib/utils.ts
msgid "12 hours"
@@ -89,7 +86,7 @@ msgstr "Acties"
msgid "Active"
msgstr "Actief"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Active Alerts"
msgstr "Actieve waarschuwingen"
@@ -116,11 +113,11 @@ msgstr "Weergaveopties voor grafieken aanpassen."
#: 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"
msgstr "Agent"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
@@ -133,7 +130,15 @@ msgstr "Melding geschiedenis"
msgid "Alerts"
msgstr "Waarschuwingen"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Alle 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"
@@ -210,29 +215,33 @@ msgstr "Binair"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Bits (Kbps, Mbps, Gbps)"
msgstr ""
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Bytes (KB/s, MB/s, GB/s)"
msgstr ""
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
msgstr "Cache / Buffers"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Annuleren"
#: src/components/routes/system/smart-table.tsx
msgid "Capacity"
msgstr "Capaciteit"
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr "Opgelet - potentieel gegevensverlies"
#: src/components/routes/settings/general.tsx
msgid "Celsius (°C)"
msgstr "Celsius (°C)"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
@@ -267,9 +276,13 @@ msgstr "Controleer de logs voor meer details."
msgid "Check your notification service"
msgstr "Controleer je meldingsservice"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Klik op een container om meer informatie te zien."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
msgstr "Klik op een apparaat om meer informatie te bekijken."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
@@ -293,14 +306,10 @@ msgstr "Configureer hoe je waarschuwingsmeldingen ontvangt."
msgid "Confirm password"
msgstr "Bevestig wachtwoord"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "Verbinding is niet actief"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,12 +365,22 @@ msgstr "Kopieer de<0>docker-compose.yml</0> inhoud voor de agent hieronder, of r
msgid "Copy YAML"
msgstr "YAML kopiëren"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr "CPU"
msgstr ""
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
msgstr "CPU-kernen"
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Time Breakdown"
msgstr "CPU-tijdverdeling"
#: 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 "Processorgebruik"
@@ -392,10 +411,14 @@ msgstr "Cumulatieve upload"
msgid "Current state"
msgstr "Huidige status"
#. Power Cycles
#: src/components/routes/system/smart-table.tsx
msgid "Cycles"
msgstr "Cycli"
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
msgid "Dashboard"
msgstr "Dashboard"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Default time period"
@@ -410,6 +433,14 @@ msgstr "Verwijderen"
msgid "Delete fingerprint"
msgstr "Vingerafdruk verwijderen"
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
msgstr ""
#: src/components/routes/system/smart-table.tsx
msgid "Device"
msgstr "Apparaat"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
@@ -423,10 +454,6 @@ msgstr "Schijf"
msgid "Disk I/O"
msgstr "Schijf I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Schijf eenheid"
@@ -445,14 +472,6 @@ msgstr "Schijfgebruik van {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU-gebruik"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker geheugengebruik"
@@ -461,14 +480,6 @@ msgstr "Docker geheugengebruik"
msgid "Docker Network I/O"
msgstr "Docker netwerk I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Documentatie"
@@ -536,7 +547,7 @@ msgstr "Fout"
#. 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 "Overschrijdt {0}{1} in de laatste {2, plural, one {# minuut} other {# minuten}}"
@@ -558,7 +569,11 @@ msgstr "Exporteer je huidige systeemconfiguratie."
#: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
msgstr ""
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Mislukte kenmerken:"
#: src/lib/api.ts
msgid "Failed to authenticate"
@@ -577,23 +592,22 @@ msgstr "Versturen test notificatie mislukt"
msgid "Failed to update alert"
msgstr "Bijwerken waarschuwing mislukt"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filter..."
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr "Vingerafdruk"
#: 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 "Voor <0>{min}</0> {min, plural, one {minuut} other {minuten}}"
@@ -631,6 +645,10 @@ msgstr "GPU stroomverbruik"
msgid "Grid"
msgstr "Raster"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
msgstr "Gezondheid"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -650,6 +668,11 @@ msgstr "Inactief"
msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Als je het wachtwoord voor je beheerdersaccount bent kwijtgeraakt, kan je het opnieuw instellen met behulp van de volgende opdracht."
#: 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 "Ongeldig e-mailadres."
@@ -657,7 +680,7 @@ msgstr "Ongeldig e-mailadres."
#. Linux kernel
#: src/components/routes/system.tsx
msgid "Kernel"
msgstr "Kernel"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Language"
@@ -702,9 +725,10 @@ msgid "Login attempt failed"
msgstr "Aanmelding mislukt"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
msgid "Logs"
msgstr "Logs"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
@@ -722,8 +746,9 @@ msgstr "Handmatige installatie-instructies"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
msgid "Max 1 min"
msgstr "Max 1 min"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Memory"
msgstr "Geheugen"
@@ -737,14 +762,20 @@ msgstr "Geheugengebruik"
msgid "Memory usage of docker containers"
msgstr "Geheugengebruik van docker containers"
#: 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 "Naam"
#: 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"
@@ -762,18 +793,22 @@ msgstr "Netwerkverkeer van publieke interfaces"
msgid "Network unit"
msgstr "Netwerk eenheid"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "Geen resultaten gevonden."
#: 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 "Geen resultaten."
#: src/components/routes/system/smart-table.tsx
msgid "No S.M.A.R.T. attributes available for this device."
msgstr "Geen S.M.A.R.T. kenmerken beschikbaar voor dit apparaat."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "No systems found."
@@ -801,16 +836,21 @@ msgstr "Eenmalig wachtwoord"
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Open menu"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Of ga verder met"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Other"
msgstr "Overig"
#: src/components/alerts/alerts-sheet.tsx
msgid "Overwrite existing alerts"
msgstr "Overschrijf bestaande waarschuwingen"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
msgstr "Pagina"
@@ -854,6 +894,15 @@ msgstr "Gepauzeerd"
msgid "Paused ({pausedSystemsLength})"
msgstr "Gepauzeerd ({pausedSystemsLength})"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
msgstr "Gemiddeld gebruik per kern"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Percentage tijd besteed in elke status"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "<0>Configureer een SMTP-server </0> om ervoor te zorgen dat waarschuwingen worden afgeleverd."
@@ -891,6 +940,11 @@ msgstr "Meld je aan bij je account"
msgid "Port"
msgstr "Poort"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
msgid "Power On"
msgstr "Inschakelen"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Precise utilization at the recorded time"
@@ -915,6 +969,11 @@ msgstr "Lezen"
msgid "Received"
msgstr "Ontvangen"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
msgid "Refresh"
msgstr "Vernieuwen"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Eenmalig wachtwoord aanvragen"
@@ -945,6 +1004,14 @@ msgstr "Roteer Token"
msgid "Rows per page"
msgstr "Rijen per pagina"
#: 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 "S.M.A.R.T. Zelf-test"
#: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Bewaar het adres met de enter-toets of komma. Laat leeg om e-mailmeldingen uit te schakelen."
@@ -974,6 +1041,10 @@ msgstr "Zie <0>notificatie-instellingen</0> om te configureren hoe je meldingen
msgid "Sent"
msgstr "Verzonden"
#: 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 "Stel percentagedrempels in voor meterkleuren."
@@ -1006,10 +1077,12 @@ msgstr "Sorteren op"
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"
msgstr "Status"
msgstr ""
#: src/components/routes/system.tsx
msgid "Swap space used by the system"
@@ -1020,6 +1093,7 @@ msgid "Swap Usage"
msgstr "Swap gebruik"
#: 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
@@ -1030,10 +1104,6 @@ msgstr "Systeem"
msgid "System load averages over time"
msgstr "Gemiddelde systeembelasting na verloop van tijd"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systemen"
@@ -1047,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 "Temperatuur"
@@ -1066,7 +1137,7 @@ msgstr "Temperatuur van systeem sensoren"
#: src/components/routes/settings/notifications.tsx
msgid "Test <0>URL</0>"
msgstr "Test <0>URL</0>"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Test notification sent"
@@ -1128,6 +1199,11 @@ msgstr "Tokens staan agenten toe om verbinding te maken met en te registreren. V
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Totaal"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Totaal ontvangen gegevens per interface"
@@ -1172,6 +1248,10 @@ msgstr "Triggert wanneer de status schakelt tussen up en down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrijdt"
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr ""
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
@@ -1197,6 +1277,10 @@ msgstr "Online"
msgid "Up ({upSystemsLength})"
msgstr "Online ({upSystemsLength})"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Updated"
msgstr "Bijgewerkt"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Uploaden"
@@ -1209,6 +1293,7 @@ msgstr "Actief"
#: 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 "Gebruik"
@@ -1234,6 +1319,7 @@ msgstr "Waarde"
msgid "View"
msgstr "Weergave"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Meer weergeven"
@@ -1246,10 +1332,6 @@ msgstr "Bekijk je 200 meest recente meldingen."
msgid "Visible Fields"
msgstr "Zichtbare kolommen"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Wachtend op genoeg records om weer te geven"

View File

@@ -8,36 +8,33 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-06 07:37\n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: no\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 {# dag} other {# dager}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# time} other {# timer}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
"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} av {1} rad(er) valgt."
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
msgstr "{count, plural, one {{countString} dag} other {{countString} dager}}"
#: 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} minutt} other {{countString} minutter}}"
#: src/lib/utils.ts
msgid "1 hour"
@@ -63,7 +60,7 @@ msgstr "12 timer"
#. 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 dager"
#. 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 "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"
@@ -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 "Varselhistorikk"
#: 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."
@@ -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 "Avbryt"
#: src/components/routes/system/smart-table.tsx
msgid "Capacity"
msgstr "Kapasitet"
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr "Advarsel - potensielt tap av 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 "Endre måleenheter for målinger."
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
@@ -267,13 +276,17 @@ msgstr "Sjekk loggene for flere detaljer."
msgid "Check your notification service"
msgstr "Sjekk din meldingstjeneste"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Klikk på en container for å se mer informasjon."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
msgstr "Klikk på en enhet for å se mer informasjon."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
msgstr "Klikk på et system for å se mer informasjon."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
@@ -293,13 +306,9 @@ msgstr "Konfigurer hvordan du vil motta alarmvarsler."
msgid "Confirm password"
msgstr "Bekreft passord"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
msgstr "Tilkoblingen er nede"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -325,7 +334,7 @@ msgstr "Kopier docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr ""
msgstr "Kopier env"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
@@ -346,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 installasjonskommandoen for agenten nedenfor, eller registrer agenter automatisk med en <0>universal 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 ""
msgstr "Kopier <0>docker-compose.yml</0> for agenten nedenfor, eller registrer agenter automatisk med en <0>universal token</0>."
#: 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-kjerner"
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Time Breakdown"
msgstr "CPU-tidsoppdeling"
#: 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-bruk"
@@ -373,7 +392,7 @@ msgstr "Opprett konto"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr ""
msgstr "Opprettet"
#: src/components/routes/settings/general.tsx
msgid "Critical (%)"
@@ -392,8 +411,12 @@ msgstr "Kumulativ opplasting"
msgid "Current state"
msgstr "Nåværende tilstand"
#. Power Cycles
#: src/components/routes/system/smart-table.tsx
msgid "Cycles"
msgstr "Sykluser"
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
msgid "Dashboard"
msgstr "Dashbord"
@@ -408,7 +431,15 @@ msgstr "Slett"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint"
msgstr ""
msgstr "Slett fingeravtrykk"
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
msgstr "Detaljer"
#: src/components/routes/system/smart-table.tsx
msgid "Device"
msgstr "Enhet"
#. Context: Battery state
#: src/lib/i18n.ts
@@ -423,13 +454,9 @@ msgstr "Disk"
msgid "Disk I/O"
msgstr "Disk I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr ""
msgstr "Diskenhet"
#: src/components/charts/disk-chart.tsx
#: src/components/routes/system.tsx
@@ -445,14 +472,6 @@ msgstr "Diskbruk av {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU-bruk"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker Minnebruk"
@@ -461,14 +480,6 @@ msgstr "Docker Minnebruk"
msgid "Docker Network I/O"
msgstr "Docker Nettverks-I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentasjon"
@@ -483,7 +494,7 @@ 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"
@@ -491,7 +502,7 @@ msgstr "Last ned"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
msgstr "Varighet"
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -536,7 +547,7 @@ msgstr "Feil"
#. 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 "Overstiger {0}{1} {2, plural, one {det siste minuttet} other {de siste # minuttene}}"
@@ -546,7 +557,7 @@ msgstr "Eksisterende systemer som ikke er er definert i <0>config.yml</0> vil bl
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
msgstr ""
msgstr "Eksporter"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -558,7 +569,11 @@ msgstr "Eksporter din nåværende systemkonfigurasjon"
#: 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"
@@ -577,22 +592,21 @@ msgstr "Kunne ikke sende test-varsling"
msgid "Failed to update alert"
msgstr "Kunne ikke oppdatere alarm"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filter..."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint"
msgstr ""
msgstr "Fingeravtrykk"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr "Fastvare"
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -611,7 +625,7 @@ msgstr "FreeBSD kommando"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "Full"
msgstr "Fullt"
#. Context: General settings
#: src/components/routes/settings/general.tsx
@@ -631,6 +645,10 @@ msgstr "GPU Effektforbruk"
msgid "Grid"
msgstr "Rutenett"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
msgstr "Helse"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -650,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 "Dersom du har mistet passordet til admin-kontoen kan du nullstille det med følgende kommando."
#: 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 "Ugyldig e-postadresse."
@@ -665,28 +688,28 @@ msgstr "Språk"
#: src/components/systems-table/systems-table.tsx
msgid "Layout"
msgstr "Layout"
msgstr "Oppsett"
#: src/components/routes/system.tsx
msgid "Load Average"
msgstr ""
msgstr "Snittbelastning Last"
#: src/lib/alerts.ts
msgid "Load Average 15m"
msgstr ""
msgstr "Snittbelastning 15m"
#: src/lib/alerts.ts
msgid "Load Average 1m"
msgstr ""
msgstr "Snittbelastning 1m"
#: src/lib/alerts.ts
msgid "Load Average 5m"
msgstr ""
msgstr "Snittbelastning 5m"
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg"
msgstr ""
msgstr "Snittbelastning"
#: src/components/navbar.tsx
msgid "Log Out"
@@ -702,6 +725,7 @@ msgid "Login attempt failed"
msgstr "Innlogging mislyktes"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
msgid "Logs"
msgstr "Logger"
@@ -724,6 +748,7 @@ msgstr "Instruks for Manuell Installasjon"
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 "Minne"
@@ -737,11 +762,17 @@ msgstr "Minnebruk"
msgid "Memory usage of docker containers"
msgstr "Minnebruk av docker-konteinere"
#: 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 "Navn"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Net"
msgstr "Nett"
@@ -760,19 +791,23 @@ msgstr "Nettverkstrafikk av eksterne nettverksgrensesnitt"
#. Context: Bytes or bits
#: src/components/routes/settings/general.tsx
msgid "Network unit"
msgstr ""
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
msgstr "Nettverksenhet"
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "Ingen resultater funnet."
#: 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 tilgjengelig for denne enheten."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
@@ -807,10 +842,15 @@ msgstr "Åpne meny"
msgid "Or continue with"
msgstr "Eller fortsett 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"
@@ -819,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} av {1}"
#: src/components/command-palette.tsx
msgid "Pages / Settings"
@@ -852,7 +892,16 @@ msgstr "Satt på Pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
msgstr "Pauset ({pausedSystemsLength})"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
msgstr "Gjennomsnittlig utnyttelse per kjerne"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Prosentandel av tid brukt i hver tilstand"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -891,6 +940,11 @@ msgstr "Vennligst logg inn på kontoen din"
msgid "Port"
msgstr "Port"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
msgid "Power On"
msgstr "Påslag"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Precise utilization at the recorded time"
@@ -915,6 +969,11 @@ msgstr "Lesing"
msgid "Received"
msgstr "Mottatt"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
msgid "Refresh"
msgstr "Oppdater"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Be om engangspassord"
@@ -931,7 +990,7 @@ msgstr "Nullstill Passord"
#: 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"
@@ -939,11 +998,19 @@ msgstr "Gjenoppta"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr ""
msgstr "Forny token"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
msgstr ""
msgstr "Rader 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."
@@ -974,6 +1041,10 @@ msgstr "Se <0>varslingsinnstillingene</0> for å konfigurere hvordan du vil mott
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 "Angi prosentvise terskler for målerfarger."
@@ -1004,8 +1075,10 @@ msgstr "Sorter Etter"
#. 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"
@@ -1020,6 +1093,7 @@ msgid "Swap Usage"
msgstr "Swap-bruk"
#: 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
@@ -1030,10 +1104,6 @@ msgstr "System"
msgid "System load averages over time"
msgstr "Systembelastning gjennomsnitt over tid"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systemer"
@@ -1047,6 +1117,7 @@ msgid "Table"
msgstr "Tabell"
#. Temperature label in systems table
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Temp"
msgstr "Temp"
@@ -1058,7 +1129,7 @@ msgstr "Temperatur"
#: src/components/routes/settings/general.tsx
msgid "Temperature unit"
msgstr ""
msgstr "Temperaturenhet"
#: src/components/routes/system.tsx
msgid "Temperatures of system sensors"
@@ -1082,7 +1153,7 @@ msgstr "Denne handlingen kan ikke omgjøres. Dette vil slette alle poster for {n
#: 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 valgte oppføringer fra databasen."
#: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}"
@@ -1112,21 +1183,26 @@ msgstr "Tema av/på"
#: 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 "Tokens & Fingeravtrykk"
#: 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 "Tokens lar agenter koble til og registrere seg selv. Fingeravtrykk er stabile identifikatorer som er unike for hvert system, og blir satt ved første tilkobling."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
msgstr "Tokens og fingeravtrykk blir brukt for å autentisere WebSocket-tilkoblinger til huben."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
@@ -1138,15 +1214,15 @@ msgstr "Totalt sendt data for hvert grensesnitt"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
msgstr "Slår inn når gjennomsnittsbelastningen over 1 minutt overstiger en grenseverdi"
#: src/lib/alerts.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr ""
msgstr "Slår inn når gjennomsnittsbelastningen over 15 minutter overstiger en grenseverdi"
#: src/lib/alerts.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr ""
msgstr "Slår inn når gjennomsnittsbelastningen over 5 minutter overstiger en grenseverdi"
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1172,15 +1248,19 @@ msgstr "Slår inn når statusen veksler mellom oppe og nede"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grenseverdi"
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr ""
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
msgstr ""
msgstr "Enhetspreferanser"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
msgstr "Universal token"
#. Context: Battery state
#: src/lib/i18n.ts
@@ -1195,7 +1275,11 @@ 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 "Oppdatert"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
@@ -1209,6 +1293,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 "Forbruk"
@@ -1228,12 +1313,13 @@ msgstr "Brukere"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr ""
msgstr "Verdi"
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Visning"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Se mer"
@@ -1246,10 +1332,6 @@ msgstr "Vis de 200 siste varslene."
msgid "Visible Fields"
msgstr "Synlige Felter"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Venter på nok registreringer til å vise"
@@ -1272,7 +1354,7 @@ msgstr "Webhook / Push-varslinger"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr ""
msgstr "Når aktivert lar denne tokenen agenter registrere seg selv uten å opprettes på systemet først. Utløper etter én time eller når huben starter på nytt."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-18 15:36\n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -18,27 +18,24 @@ 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 {# dzień} few {# dni} many {# dni} other {# dni}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {godzinę} few {# godziny} many {# godzin} other {# godziny}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected."
msgstr "{0} z {1} wybranych wierszy."
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
msgstr "{count, plural, one {{countString} dzień} few {{countString} dni} many {{countString} dni} other {{countString} dni}}"
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
msgstr "{count, plural, one {godzinę} few {{countString} godziny} many {{countString} godzin} other {{countString} godziny}}"
#: 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 godzina"
@@ -89,7 +86,7 @@ msgstr "Akcje"
msgid "Active"
msgstr "Aktywny"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Active Alerts"
msgstr "Aktywne alerty"
@@ -133,7 +130,15 @@ msgstr "Historia alertów"
msgid "Alerts"
msgstr "Alerty"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Wszystkie kontenery"
#: 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 "Pamięć podręczna / Bufory"
msgid "Cancel"
msgstr "Anuluj"
#: src/components/routes/system/smart-table.tsx
msgid "Capacity"
msgstr "Pojemność"
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr "Uwaga- potencjalna utrata danych."
@@ -267,9 +276,13 @@ msgstr "Sprawdź logi, aby uzyskać więcej informacji."
msgid "Check your notification service"
msgstr "Sprawdź swój serwis powiadomień"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Kliknij na kontener, aby wyświetlić więcej informacji."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
msgstr "Kliknij na urządzenie, aby wyświetlić więcej informacji."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
@@ -293,14 +306,10 @@ msgstr "Skonfiguruj sposób otrzymywania powiadomień."
msgid "Confirm password"
msgstr "Potwierdź hasło"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "Brak połączenia"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,12 +365,22 @@ msgstr "Skopiuj poniżej zawartość pliku <0>docker-compose.yml</0> dla agenta
msgid "Copy YAML"
msgstr "Kopiuj 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 "Rdzenie CPU"
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Time Breakdown"
msgstr "Podział czasu 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 "Użycie procesora"
@@ -392,8 +411,12 @@ msgstr "Wysyłanie skumulowane"
msgid "Current state"
msgstr "Aktualny stan"
#. Power Cycles
#: src/components/routes/system/smart-table.tsx
msgid "Cycles"
msgstr "Cykle"
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
msgid "Dashboard"
msgstr "Panel kontrolny"
@@ -410,6 +433,14 @@ msgstr "Usuń"
msgid "Delete fingerprint"
msgstr "Usuń odcisk palca"
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
msgstr "Szczegół"
#: src/components/routes/system/smart-table.tsx
msgid "Device"
msgstr "Urządzenie"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
@@ -423,10 +454,6 @@ msgstr "Dysk"
msgid "Disk I/O"
msgstr "Dysk I/O"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Jednostka dysku"
@@ -445,14 +472,6 @@ msgstr "Wykorzystanie dysku {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Wykorzystanie procesora przez Docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Wykorzystanie pamięci przez Docker"
@@ -461,14 +480,6 @@ msgstr "Wykorzystanie pamięci przez Docker"
msgid "Docker Network I/O"
msgstr "Sieć Docker I/O"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentacja"
@@ -536,7 +547,7 @@ msgstr "Błąd"
#. 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 "Przekracza {0}{1} w ciągu ostatnich {2, plural, one {# minuty} other {# minut}}"
@@ -560,6 +571,10 @@ msgstr "Eksportuj aktualną konfigurację systemów."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Nieudane atrybuty:"
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Błąd autoryzacji"
@@ -577,15 +592,10 @@ msgstr "Nie udało się wysłać testowego powiadomienia"
msgid "Failed to update alert"
msgstr "Nie udało się zaktualizować powiadomienia"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filtruj..."
@@ -594,6 +604,10 @@ msgstr "Filtruj..."
msgid "Fingerprint"
msgstr "Odcisk palca"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr "Oprogramowanie sprzętowe"
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "Na <0>{min}</0> {min, plural, one {minutę} other {minut}}"
@@ -631,6 +645,10 @@ msgstr "Moc GPU"
msgid "Grid"
msgstr "Siatka"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
msgstr "Zdrowie"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -650,6 +668,11 @@ msgstr "Bezczynna"
msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Jeśli utraciłeś hasło do swojego konta administratora, możesz je zresetować, używając następującego polecenia."
#: 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 "Nieprawidłowy adres e-mail."
@@ -702,6 +725,7 @@ msgid "Login attempt failed"
msgstr "Próba logowania nie powiodła się"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
msgid "Logs"
msgstr "Logi"
@@ -724,6 +748,7 @@ msgstr "Instrukcja ręcznej konfiguracji"
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 "Pamięć"
@@ -737,11 +762,17 @@ msgstr "Wykorzystanie pamięci"
msgid "Memory usage of docker containers"
msgstr "Użycie pamięci przez kontenery Docker."
#: 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 "Nazwa"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Net"
msgstr "Sieć"
@@ -762,18 +793,22 @@ msgstr "Ruch sieciowy interfejsów publicznych"
msgid "Network unit"
msgstr "Jednostka sieciowa"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "Brak wyników."
#: 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 "Brak wyników."
#: src/components/routes/system/smart-table.tsx
msgid "No S.M.A.R.T. attributes available for this device."
msgstr "Brak dostępnych atrybutów S.M.A.R.T. dla tego urządzenia."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "No systems found."
@@ -807,10 +842,15 @@ msgstr "Otwórz menu"
msgid "Or continue with"
msgstr "Lub kontynuuj z"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Other"
msgstr "Inne"
#: src/components/alerts/alerts-sheet.tsx
msgid "Overwrite existing alerts"
msgstr "Nadpisz istniejące alerty"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
msgstr "Strona"
@@ -854,6 +894,15 @@ msgstr "Wstrzymane"
msgid "Paused ({pausedSystemsLength})"
msgstr "Wstrzymane ({pausedSystemsLength})"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
msgstr "Średnie wykorzystanie na rdzeń"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Procent czasu spędzonego w każdym stanie"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Proszę <0>skonfigurować serwer SMTP</0>, aby zapewnić dostarczanie powiadomień."
@@ -891,6 +940,11 @@ msgstr "Zaloguj się na swoje konto"
msgid "Port"
msgstr "Port"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
msgid "Power On"
msgstr "Włączony"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Precise utilization at the recorded time"
@@ -915,6 +969,11 @@ msgstr "Odczyt"
msgid "Received"
msgstr "Otrzymane"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
msgid "Refresh"
msgstr "Odśwież"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Zażądaj jednorazowego hasła"
@@ -945,6 +1004,14 @@ msgstr "Zmień token"
msgid "Rows per page"
msgstr "Wiersze na stronę"
#: src/components/routes/system/smart-table.tsx
msgid "S.M.A.R.T. Details"
msgstr "Szczegóły S.M.A.R.T."
#: src/components/routes/system/smart-table.tsx
msgid "S.M.A.R.T. Self-Test"
msgstr "Samodiagnostyka 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 "Zapisz adres, używając klawisza enter lub przecinka. Pozostaw puste, aby wyłączyć powiadomienia e-mail."
@@ -974,6 +1041,10 @@ msgstr "Zobacz <0>ustawienia powiadomień</0>, aby skonfigurować sposób, w jak
msgid "Sent"
msgstr "Wysłane"
#: src/components/routes/system/smart-table.tsx
msgid "Serial Number"
msgstr "Numer seryjny"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Ustaw progi procentowe dla kolorów mierników."
@@ -1006,6 +1077,8 @@ msgstr "Sortuj według"
msgid "State"
msgstr "Stan"
#: 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"
@@ -1020,6 +1093,7 @@ msgid "Swap Usage"
msgstr "Użycie pamięci wymiany"
#: 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
@@ -1030,10 +1104,6 @@ msgstr "System"
msgid "System load averages over time"
msgstr "Średnie obciążenie systemu w czasie"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Systemy"
@@ -1047,6 +1117,7 @@ msgid "Table"
msgstr "Tabela"
#. Temperature label in systems table
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Temp"
msgstr "Temperatura"
@@ -1128,6 +1199,11 @@ msgstr "Tokeny umożliwiają agentom łączenie się i rejestrację. Odciski pal
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny i odciski palców (fingerprinty) służą do uwierzytelniania połączeń WebSocket z hubem."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Łącznie"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Całkowita ilość danych odebranych dla każdego interfejsu"
@@ -1172,6 +1248,10 @@ msgstr "Wyzwalane, gdy status przełącza się między stanem aktywnym a nieakty
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Wyzwalane, gdy wykorzystanie któregokolwiek dysku przekroczy ustalony próg"
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Typ"
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
@@ -1197,6 +1277,10 @@ msgstr "Działa"
msgid "Up ({upSystemsLength})"
msgstr "Działa ({upSystemsLength})"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Updated"
msgstr "Zaktualizowano"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Wysyłanie"
@@ -1209,6 +1293,7 @@ msgstr "Czas pracy"
#: 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 "Wykorzystanie"
@@ -1234,6 +1319,7 @@ msgstr "Wartość"
msgid "View"
msgstr "Widok"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Zobacz więcej"
@@ -1246,10 +1332,6 @@ msgstr "Wyświetl 200 ostatnich alertów."
msgid "Visible Fields"
msgstr "Widoczne kolumny"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Oczekiwanie na wystarczającą liczbę rekordów do wyświetlenia"

View File

@@ -8,30 +8,15 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-09 12:03\n"
"PO-Revision-Date: 2025-11-04 22:13\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: pt-PT\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 {# dia} other {# dias}}"
#. 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} linha(s) selecionada(s)."
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
msgstr "{count, plural, one {{countString} dia} other {{countString} dias}}"
#: 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,7 +86,7 @@ msgstr "Ações"
msgid "Active"
msgstr "Ativo"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Active Alerts"
msgstr "Alertas Ativos"
@@ -133,7 +130,15 @@ msgstr "Histórico 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 os Contêineres"
#: 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"
@@ -219,13 +224,17 @@ msgstr "Bytes (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers"
msgstr "Cache / Buffers"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Cancelar"
#: src/components/routes/system/smart-table.tsx
msgid "Capacity"
msgstr "Capacidade"
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr "Cuidado - possível perda de dados"
@@ -267,9 +276,13 @@ msgstr "Verifique os logs para mais detalhes."
msgid "Check your notification service"
msgstr "Verifique seu serviço de notificação"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Clique num contentor para ver mais informações."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
msgstr "Clique em um dispositivo para ver mais informações."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
@@ -293,14 +306,10 @@ msgstr "Configure como você recebe notificações de alerta."
msgid "Confirm password"
msgstr "Confirmar senha"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "A conexão está inativa"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,12 +365,22 @@ msgstr "Copie o conteúdo do <0>docker-compose.yml</0> do agente abaixo, ou regi
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 "Distribuição do Tempo 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"
@@ -392,8 +411,12 @@ msgstr "Upload cumulativo"
msgid "Current state"
msgstr "Estado atual"
#. 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 "Painel"
@@ -410,6 +433,14 @@ msgstr "Excluir"
msgid "Delete fingerprint"
msgstr "Excluir impressão digital"
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
msgstr "Detalhe"
#: src/components/routes/system/smart-table.tsx
msgid "Device"
msgstr "Dispositivo"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
@@ -423,10 +454,6 @@ msgstr "Disco"
msgid "Disk I/O"
msgstr "E/S de Disco"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Unidade de disco"
@@ -445,14 +472,6 @@ msgstr "Uso de disco de {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Uso de CPU do Docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Uso de Memória do Docker"
@@ -461,14 +480,6 @@ msgstr "Uso de Memória do Docker"
msgid "Docker Network I/O"
msgstr "E/S de Rede do Docker"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Documentação"
@@ -479,7 +490,7 @@ msgstr "Documentação"
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Desligado"
msgstr "Desligado"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
@@ -536,7 +547,7 @@ msgstr "Erro"
#. 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} no último {2, plural, one {# minuto} other {# minutos}}"
@@ -560,6 +571,10 @@ msgstr "Exporte a configuração atual dos seus sistemas."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Atributos com Falha:"
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Falha na autenticação"
@@ -577,15 +592,10 @@ msgstr "Falha ao enviar notificação de teste"
msgid "Failed to update alert"
msgstr "Falha ao atualizar alerta"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filtrar..."
@@ -594,6 +604,10 @@ msgstr "Filtrar..."
msgid "Fingerprint"
msgstr "Impressão digital"
#: 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}}"
@@ -631,6 +645,10 @@ msgstr "Consumo de Energia da GPU"
msgid "Grid"
msgstr "Grade"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
msgstr "Saúde"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -639,7 +657,7 @@ msgstr "Comando Homebrew"
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr "Host / IP"
msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
@@ -650,6 +668,11 @@ msgstr "Inativa"
msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Se você perdeu a senha da sua conta de administrador, pode redefini-la usando o seguinte comando."
#: src/components/containers-table/containers-table-columns.tsx
msgctxt "Docker image"
msgid "Image"
msgstr "Imagem"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Endereço de email inválido."
@@ -657,7 +680,7 @@ msgstr "Endereço de email inválido."
#. Linux kernel
#: src/components/routes/system.tsx
msgid "Kernel"
msgstr "Kernel"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Language"
@@ -702,9 +725,10 @@ msgid "Login attempt failed"
msgstr "Tentativa de login falhou"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
msgid "Logs"
msgstr "Logs"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
@@ -724,6 +748,7 @@ msgstr "Instruções de configuração manual"
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 "Memória"
@@ -737,11 +762,17 @@ msgstr "Uso de Memória"
msgid "Memory usage of docker containers"
msgstr "Uso de memória dos contêineres 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 "Nome"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Net"
msgstr "Rede"
@@ -762,18 +793,22 @@ msgstr "Tráfego de rede das interfaces públicas"
msgid "Network unit"
msgstr "Unidade de rede"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "Nenhum resultado encontrado."
#: 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 "Sem resultados."
#: src/components/routes/system/smart-table.tsx
msgid "No S.M.A.R.T. attributes available for this device."
msgstr "Nenhum atributo S.M.A.R.T. disponível para este dispositivo."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "No systems found."
@@ -807,10 +842,15 @@ msgstr "Abrir menu"
msgid "Or continue with"
msgstr "Ou continue com"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Other"
msgstr "Outro"
#: src/components/alerts/alerts-sheet.tsx
msgid "Overwrite existing alerts"
msgstr "Sobrescrever alertas existentes"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
msgstr "Página"
@@ -854,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 "Utilização média por núcleo"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Percentagem de tempo gasto em 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 um servidor SMTP</0> para garantir que os alertas sejam entregues."
@@ -891,6 +940,11 @@ msgstr "Por favor, entre na sua conta"
msgid "Port"
msgstr "Porta"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
msgid "Power On"
msgstr "Ligado"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Precise utilization at the recorded time"
@@ -915,6 +969,11 @@ msgstr "Ler"
msgid "Received"
msgstr "Recebido"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
msgid "Refresh"
msgstr "Atualizar"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Solicitar senha de uso único"
@@ -945,6 +1004,14 @@ msgstr "Rotacionar token"
msgid "Rows per page"
msgstr "Linhas por página"
#: src/components/routes/system/smart-table.tsx
msgid "S.M.A.R.T. Details"
msgstr "Detalhes S.M.A.R.T."
#: src/components/routes/system/smart-table.tsx
msgid "S.M.A.R.T. Self-Test"
msgstr "Auto-teste 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 "Salve o endereço usando a tecla enter ou vírgula. Deixe em branco para desativar notificações por email."
@@ -974,6 +1041,10 @@ msgstr "Veja <0>configurações de notificação</0> para configurar como você
msgid "Sent"
msgstr "Enviado"
#: src/components/routes/system/smart-table.tsx
msgid "Serial Number"
msgstr "Número de Série"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Defina os limiares de porcentagem para as cores do medidor."
@@ -1006,10 +1077,12 @@ 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"
msgstr "Status"
msgstr "Estado"
#: src/components/routes/system.tsx
msgid "Swap space used by the system"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "Sistema"
msgid "System load averages over time"
msgstr "Médias de carga do sistema ao longo do tempo"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Sistemas"
@@ -1047,9 +1117,10 @@ msgid "Table"
msgstr "Tabela"
#. Temperature label in systems table
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Temp"
msgstr "Temp"
msgstr ""
#: src/components/routes/system.tsx
#: src/lib/alerts.ts
@@ -1112,7 +1183,7 @@ msgstr "Alternar tema"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1128,6 +1199,11 @@ msgstr "Os tokens permitem que os agentes se conectem e registrem. As impressõe
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens e impressões digitais são usados para autenticar conexões WebSocket ao hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Dados totais recebidos para cada interface"
@@ -1172,6 +1248,10 @@ msgstr "Dispara quando o status alterna entre ativo e inativo"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Dispara quando o uso de qualquer disco excede um limite"
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Tipo"
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
@@ -1191,12 +1271,16 @@ msgstr "Desconhecida"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Up"
msgstr "Ligado"
msgstr "Ligado"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr "Ativo ({upSystemsLength})"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Updated"
msgstr "Atualizado"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr "Carregar"
@@ -1209,6 +1293,7 @@ msgstr "Tempo de Atividade"
#: 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"
@@ -1234,6 +1319,7 @@ msgstr "Valor"
msgid "View"
msgstr "Visual"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Ver mais"
@@ -1246,10 +1332,6 @@ msgstr "Veja os seus 200 alertas mais recentes."
msgid "Visible Fields"
msgstr "Campos Visíveis"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Aguardando registros suficientes para exibir"

File diff suppressed because it is too large Load Diff

View File

@@ -8,30 +8,15 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-28 07:31\n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: ru\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,9 +276,13 @@ msgstr "Проверьте журналы для получения более
msgid "Check your notification service"
msgstr "Проверьте ваш сервис уведомлений"
#: src/components/routes/system.tsx
msgid "Clear all"
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."
@@ -293,14 +306,10 @@ msgstr "Настройте, как вы получаете уведомлени
msgid "Confirm password"
msgstr "Подтвердите пароль"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr "Нет соединения"
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Continue"
@@ -356,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 "Использование CPU"
@@ -392,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 "Панель управления"
@@ -410,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"
@@ -423,10 +454,6 @@ msgstr "Диск"
msgid "Disk I/O"
msgstr "Дисковый ввод/вывод"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
msgstr "Единицы измерения температуры"
@@ -445,14 +472,6 @@ msgstr "Использование диска {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Использование CPU Docker"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Использование памяти Docker"
@@ -461,14 +480,6 @@ msgstr "Использование памяти Docker"
msgid "Docker Network I/O"
msgstr "Сетевой ввод/вывод Docker"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Документация"
@@ -536,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 {# минут}}"
@@ -560,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 "Не удалось аутентифицировать"
@@ -577,15 +592,10 @@ msgstr "Не удалось отправить тестовое уведомле
msgid "Failed to update alert"
msgstr "Не удалось обновить оповещение"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Фильтр..."
@@ -594,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 {минут}}"
@@ -631,6 +645,10 @@ msgstr "Потребляемая мощность GPU"
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"
@@ -650,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 "Неверный адрес электронной почты."
@@ -702,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 "Журналы"
@@ -724,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 "Память"
@@ -737,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 "Сеть"
@@ -762,18 +793,22 @@ msgstr "Сетевой трафик публичных интерфейсов"
msgid "Network unit"
msgstr "Единицы измерения скорости сети"
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
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."
@@ -807,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 "Страница"
@@ -854,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>, чтобы гарантировать доставку оповещений."
@@ -891,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"
@@ -915,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 "Запросить одноразовый пароль"
@@ -945,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 "Сохраните адрес, используя клавишу ввода или запятую. Оставьте пустым, чтобы отключить уведомления по электронной почте."
@@ -974,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 "Установите процентные пороги для цветов счетчиков."
@@ -1006,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"
@@ -1020,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
@@ -1030,10 +1104,6 @@ msgstr "Система"
msgid "System load averages over time"
msgstr "Средняя загрузка системы за время"
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
#: src/components/navbar.tsx
msgid "Systems"
msgstr "Системы"
@@ -1047,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 "Темп"
@@ -1128,6 +1199,11 @@ msgstr "Токены позволяют агентам подключаться
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токены и отпечатки используются для аутентификации соединений WebSocket с хабом."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Итого"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr "Общий объем полученных данных для каждого интерфейса"
@@ -1172,6 +1248,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"
@@ -1197,6 +1277,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 "Загрузить"
@@ -1209,6 +1293,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 "Использование"
@@ -1234,6 +1319,7 @@ msgstr "Значение"
msgid "View"
msgstr "Вид"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Показать больше"
@@ -1246,10 +1332,6 @@ msgstr "Просмотреть 200 последних оповещений."
msgid "Visible Fields"
msgstr "Видимые столбцы"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Ожидание достаточного количества записей для отображения"

View File

@@ -8,30 +8,15 @@ msgstr ""
"Language: sl\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-25 17:11\n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: sl\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 {# dan} two {# dneva} few {# dni} other {# dni}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ura} two {# uri} few {# ur} other {# ur}}"
#. 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 {# minuti} 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 {# minuti} many {# minut} other {# minut}
msgid "{0} of {1} row(s) selected."
msgstr "{0} od {1} vrstic izbranih."
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
msgstr "{count, plural, one {{countString} dan} two {{countString} dneva} few {{countString} dni} other {{countString} dni}}"
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} hour} other {{countString} hours}}"
msgstr "{count, plural, one {{countString} ura} two {{countString} uri} few {{countString} ur} other {{countString} ur}}"
#: 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} minuti} many {{countString} minut} other {{countString} minut}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 ura"
@@ -63,7 +60,7 @@ msgstr "12 ur"
#. 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 dni"
#. 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 "Dejanja"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active"
msgstr ""
msgstr "Aktivno"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Active Alerts"
msgstr "Aktivna opozorila"
@@ -120,20 +117,28 @@ msgstr "Administrator"
#: 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
#: src/components/routes/settings/layout.tsx
msgid "Alert History"
msgstr ""
msgstr "Zgodovina opozoril"
#: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx
msgid "Alerts"
msgstr "Opozorila"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/containers.tsx
msgid "All Containers"
msgstr "Vsi kontejnerji"
#: 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 "Ali ste prepričani, da želite izbrisati {name}?"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Are you sure?"
msgstr ""
msgstr "Ali ste prepričani?"
#: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context."
@@ -226,6 +231,10 @@ msgstr "Predpomnilnik / medpomnilniki"
msgid "Cancel"
msgstr "Prekliči"
#: src/components/routes/system/smart-table.tsx
msgid "Capacity"
msgstr "Kapaciteta"
#: src/components/routes/settings/config-yaml.tsx
msgid "Caution - potential data loss"
msgstr "Pozor - možna izguba podatkov"
@@ -236,7 +245,7 @@ msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Change display units for metrics."
msgstr ""
msgstr "Spremenite enote prikaza za metrike."
#: src/components/routes/settings/general.tsx
msgid "Change general application options."
@@ -267,13 +276,17 @@ msgstr "Za več podrobnosti preverite dnevnike."
msgid "Check your notification service"
msgstr "Preverite storitev obveščanja"
#: src/components/routes/system.tsx
msgid "Clear all"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Kliknite na kontejner za več informacij."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
msgstr "Kliknite na napravo za več informacij."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
msgstr "Kliknite na sistem za več informacij."
#: src/components/ui/input-copy.tsx
msgid "Click to copy"
@@ -293,13 +306,9 @@ msgstr "Nastavi način prejemanja opozorilnih obvestil."
msgid "Confirm password"
msgstr "Potrdite geslo"
#: src/components/routes/home.tsx
#: src/components/active-alerts.tsx
msgid "Connection is down"
msgstr ""
#: src/components/routes/system.tsx
msgid "Container health status and uptime"
msgstr ""
msgstr "Povezava je prekinjena"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -325,7 +334,7 @@ msgstr "Kopiraj docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables"
msgid "Copy env"
msgstr ""
msgstr "Kopiraj okoljske spremenljivke"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host"
@@ -346,22 +355,32 @@ msgstr "Kopiraj besedilo"
#: src/components/add-system.tsx
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
msgstr ""
msgstr "Kopirajte namestitveni ukaz za agenta spodaj ali registrirajte agente samodejno z <0>univerzalnim žetonom</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<0>docker-compose.yml</0> vsebino za agenta spodaj ali registrirajte agente samodejno z <1>univerzalnim žetonom</1>."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML"
msgstr ""
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "CPU"
msgstr "CPU"
msgstr ""
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores"
msgstr "CPU jedra"
#: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Time Breakdown"
msgstr "Razčlenitev časa 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 "CPU poraba"
@@ -373,7 +392,7 @@ msgstr "Ustvari račun"
#. Context: date created
#: src/components/alerts-history-columns.tsx
msgid "Created"
msgstr ""
msgstr "Ustvarjeno"
#: src/components/routes/settings/general.tsx
msgid "Critical (%)"
@@ -392,8 +411,12 @@ msgstr "Kumulativno nalaganje"
msgid "Current state"
msgstr "Trenutno stanje"
#. Power Cycles
#: src/components/routes/system/smart-table.tsx
msgid "Cycles"
msgstr "Cikli"
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
msgid "Dashboard"
msgstr "Nadzorna plošča"
@@ -408,7 +431,15 @@ msgstr "Izbriši"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint"
msgstr ""
msgstr "Izbriši prstni odtis"
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
msgstr "Podrobnost"
#: src/components/routes/system/smart-table.tsx
msgid "Device"
msgstr "Naprava"
#. Context: Battery state
#: src/lib/i18n.ts
@@ -417,14 +448,10 @@ msgstr "Prazni se"
#: 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"
#: src/components/routes/system.tsx
msgid "Disk read/write rates of docker containers"
msgstr ""
#: src/components/routes/settings/general.tsx
@@ -445,14 +472,6 @@ msgstr "Poraba diska za {extraFsName}"
msgid "Docker CPU Usage"
msgstr "Docker CPU poraba"
#: src/components/routes/system.tsx
msgid "Docker Disk I/O"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Health & Uptime"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Memory Usage"
msgstr "Docker poraba spomina"
@@ -461,14 +480,6 @@ msgstr "Docker poraba spomina"
msgid "Docker Network I/O"
msgstr "Docker I/O mreže"
#: src/components/routes/system.tsx
msgid "Docker Stats"
msgstr ""
#: src/components/routes/system.tsx
msgid "Docker Volumes"
msgstr ""
#: src/components/command-palette.tsx
msgid "Documentation"
msgstr "Dokumentacija"
@@ -479,11 +490,11 @@ msgstr "Dokumentacija"
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr ""
msgstr "Nedelujoč"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
msgstr "Nedelujoči ({downSystemsLength})"
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
@@ -491,12 +502,12 @@ msgstr "Prenesi"
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
msgstr "Trajanje"
#: src/components/add-system.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr ""
msgstr "Uredi"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -536,7 +547,7 @@ msgstr "Napaka"
#. 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 "Preseženo {0}{1} v zadnjih {2, plural, one {# minuti} other {# minutah}}"
@@ -546,7 +557,7 @@ msgstr "Obstoječi sistemi, ki niso definirani v <0>config.yml</0>, bodo izbrisa
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export"
msgstr ""
msgstr "Izvozi"
#: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration"
@@ -560,6 +571,10 @@ msgstr "Izvozi trenutne nastavitve sistema."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Neuspeli atributi:"
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Preverjanje pristnosti ni uspelo"
@@ -577,23 +592,22 @@ msgstr "Pošiljanje testnega obvestila ni uspelo"
msgid "Failed to update alert"
msgstr "Opozorila ni bilo mogoče posodobiti"
#: src/components/routes/system.tsx
msgid "Filter containers..."
msgstr ""
#: src/components/routes/system.tsx
msgid "Filter stacks..."
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 "Filter..."
msgstr "Filtriraj..."
#: src/components/routes/settings/tokens-fingerprints.tsx
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 "Za <0>{min}</0> {min, plural, one {minuto} other {minut}}"
@@ -631,6 +645,10 @@ msgstr "GPU poraba moči"
msgid "Grid"
msgstr "Mreža"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Health"
msgstr "Zdravje"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
@@ -650,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 "Če ste izgubili geslo za svoj skrbniški račun, ga lahko ponastavite z naslednjim ukazom."
#: 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 "Napačen e-poštni naslov."
@@ -669,24 +692,24 @@ msgstr "Postavitev"
#: src/components/routes/system.tsx
msgid "Load Average"
msgstr ""
msgstr "Povprečna obremenitev"
#: src/lib/alerts.ts
msgid "Load Average 15m"
msgstr ""
msgstr "Povprečna obremenitev 15m"
#: src/lib/alerts.ts
msgid "Load Average 1m"
msgstr ""
msgstr "Povprečna obremenitev 1m"
#: src/lib/alerts.ts
msgid "Load Average 5m"
msgstr ""
msgstr "Povprečna obremenitev 5m"
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg"
msgstr ""
msgstr "Povpr. obrem."
#: src/components/navbar.tsx
msgid "Log Out"
@@ -702,6 +725,7 @@ msgid "Login attempt failed"
msgstr "Poskus prijave ni uspel"
#: src/components/command-palette.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/navbar.tsx
msgid "Logs"
msgstr "Dnevniki"
@@ -717,13 +741,14 @@ msgstr "Upravljajte nastavitve prikaza in obvestil."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
msgstr "Navodila za ročno nastavitev"
#. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx
msgid "Max 1 min"
msgstr "Največ 1 min"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Memory"
msgstr "Pomnilnik"
@@ -737,11 +762,17 @@ msgstr "Poraba pomnilnika"
msgid "Memory usage of docker containers"
msgstr "Poraba pomnilnika docker kontejnerjev"
#: 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 "Naziv"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Net"
msgstr "Mreža"
@@ -762,17 +793,21 @@ msgstr "Omrežni promet javnih vmesnikov"
msgid "Network unit"
msgstr ""
#: src/components/routes/system.tsx
msgid "No items available"
msgstr ""
#: src/components/command-palette.tsx
msgid "No results found."
msgstr "Ni rezultatov."
#: 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 "Ni rezultatov."
#: src/components/routes/system/smart-table.tsx
msgid "No S.M.A.R.T. attributes available for this device."
msgstr "Za to napravo ni na voljo atributov S.M.A.R.T."
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
@@ -807,10 +842,15 @@ msgstr "Odpri menu"
msgid "Or continue with"
msgstr "Ali nadaljuj z"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Other"
msgstr "Drugo"
#: src/components/alerts/alerts-sheet.tsx
msgid "Overwrite existing alerts"
msgstr "Prepiši obstoječe alarme"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
msgstr "Stran"
@@ -819,7 +859,7 @@ msgstr "Stran"
#. placeholder {1}: table.getPageCount()
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Page {0} of {1}"
msgstr ""
msgstr "Stran {0} od {1}"
#: src/components/command-palette.tsx
msgid "Pages / Settings"
@@ -836,7 +876,7 @@ msgstr "Geslo mora imeti vsaj 8 znakov."
#: src/components/login/auth-form.tsx
msgid "Password must be less than 72 bytes."
msgstr ""
msgstr "Geslo mora biti krajše od 72 bajtov."
#: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received"
@@ -854,6 +894,15 @@ msgstr "Zaustavljeno"
msgid "Paused ({pausedSystemsLength})"
msgstr "Pavzirano za {pausedSystemsLength}"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization"
msgstr "Povprečna izkoriščenost na jedro"
#: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state"
msgstr "Odstotek časa, preživetega v vsakem stanju"
#: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "<0>Nastavite strežnik SMTP</0>, da zagotovite dostavo opozoril."
@@ -891,6 +940,11 @@ msgstr "Prijavite se v svoj račun"
msgid "Port"
msgstr "Vrata"
#. Power On Time
#: src/components/routes/system/smart-table.tsx
msgid "Power On"
msgstr "Vklopljeno"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
msgid "Precise utilization at the recorded time"
@@ -915,6 +969,11 @@ msgstr "Preberano"
msgid "Received"
msgstr "Prejeto"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
msgid "Refresh"
msgstr "Osveži"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr "Zahtevaj enkratno geslo"
@@ -931,7 +990,7 @@ msgstr "Ponastavi geslo"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Resolved"
msgstr ""
msgstr "Rešeno"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Resume"
@@ -939,11 +998,19 @@ msgstr "Nadaljuj"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
msgstr ""
msgstr "Zavrti žeton"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page"
msgstr ""
msgstr "Vrstic na stran"
#: src/components/routes/system/smart-table.tsx
msgid "S.M.A.R.T. Details"
msgstr "S.M.A.R.T. podrobnosti"
#: 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."
@@ -956,7 +1023,7 @@ msgstr "Shrani nastavitve"
#: src/components/add-system.tsx
msgid "Save system"
msgstr ""
msgstr "Shrani sistem"
#: src/components/navbar.tsx
msgid "Search"
@@ -974,6 +1041,10 @@ msgstr "Glejte <0>nastavitve obvestil</0>, da nastavite način prejemanja opozor
msgid "Sent"
msgstr "Poslano"
#: src/components/routes/system/smart-table.tsx
msgid "Serial Number"
msgstr "Serijska številka"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Nastavite odstotne pragove za barve merilnikov."
@@ -1004,12 +1075,14 @@ msgstr "Razvrsti 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"
msgstr "Status"
msgstr ""
#: src/components/routes/system.tsx
msgid "Swap space used by the system"
@@ -1020,6 +1093,7 @@ msgid "Swap Usage"
msgstr "Swap uporaba"
#: 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
@@ -1028,11 +1102,7 @@ msgstr "Sistemsko"
#: src/components/routes/system.tsx
msgid "System load averages over time"
msgstr ""
#: src/components/routes/system.tsx
msgid "System Stats"
msgstr ""
msgstr "Sistemske povprečne obremenitve skozi čas"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1047,9 +1117,10 @@ msgid "Table"
msgstr "Tabela"
#. 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
@@ -1082,7 +1153,7 @@ msgstr "Tega dejanja ni mogoče razveljaviti. To bo trajno izbrisalo vse trenutn
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "This will permanently delete all selected records from the database."
msgstr ""
msgstr "To bo trajno izbrisalo vse izbrane zapise iz zbirke podatkov."
#: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}"
@@ -1112,21 +1183,26 @@ msgstr "Obrni temo"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr ""
msgstr "Žeton"
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens & Fingerprints"
msgstr ""
msgstr "Žetoni in prstni odtisi"
#: 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 "Žetoni omogočajo agentom povezavo in registracijo. Prstni odtisi so stabilni identifikatorji, edinstveni za vsak sistem, nastavljeni ob prvi povezavi."
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
msgstr "Žetoni in prstni odtisi se uporabljajo za preverjanje pristnosti WebSocket povezav do vozlišča."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Skupaj"
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
@@ -1138,15 +1214,15 @@ msgstr "Skupni poslani podatki za vsak vmesnik"
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
msgstr "Sproži se, ko 1-minutna povprečna obremenitev preseže prag"
#: src/lib/alerts.ts
msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr ""
msgstr "Sproži se, ko 15-minutna povprečna obremenitev preseže prag"
#: src/lib/alerts.ts
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr ""
msgstr "Sproži se, ko 5-minutna povprečna obremenitev preseže prag"
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
@@ -1172,15 +1248,19 @@ msgstr "Sproži se, ko se stanje preklaplja med gor in dol"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Sproži se, ko uporaba katerega koli diska preseže prag"
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Vrsta"
#. Temperature / network units
#: src/components/routes/settings/general.tsx
msgid "Unit preferences"
msgstr ""
msgstr "Nastavitve enot"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token"
msgstr ""
msgstr "Univerzalni žeton"
#. Context: Battery state
#: src/lib/i18n.ts
@@ -1191,11 +1271,15 @@ msgstr "Neznana"
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Up"
msgstr ""
msgstr "Delujoč"
#: src/components/systems-table/systems-table.tsx
msgid "Up ({upSystemsLength})"
msgstr ""
msgstr "Delujoči ({upSystemsLength})"
#: src/components/containers-table/containers-table-columns.tsx
msgid "Updated"
msgstr "Posodobljeno"
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
@@ -1209,6 +1293,7 @@ msgstr "Čas delovanja"
#: 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 "Uporaba"
@@ -1228,28 +1313,25 @@ msgstr "Uporabniki"
#: src/components/alerts-history-columns.tsx
msgid "Value"
msgstr ""
msgstr "Vrednost"
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Pogled"
#: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr "Prikaži več"
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""
msgstr "Oglejte si svojih 200 najnovejših opozoril."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Vidna polja"
#: src/components/routes/system.tsx
msgid "Volume usage of docker containers"
msgstr ""
#: src/components/routes/system.tsx
msgid "Waiting for enough records to display"
msgstr "Čakam na dovolj zapisov za prikaz"
@@ -1272,7 +1354,7 @@ msgstr "Webhook / potisna obvestila"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr ""
msgstr "Ko je omogočeno, ta žeton omogoča agentom samoregistracijo brez predhodnega ustvarjanja sistema. Poteče po eni uri ali ob ponovnem zagonu vozlišča."
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx

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