mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 05:56:17 +01:00
Compare commits
53 Commits
nvml
...
41f3705b6b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f3705b6b | ||
|
|
20324763d2 | ||
|
|
70f85f9590 | ||
|
|
c7f7f51c99 | ||
|
|
6723ec8ea4 | ||
|
|
afc19ebd3b | ||
|
|
c83d00ccaa | ||
|
|
425c8d2bdf | ||
|
|
42da1e5a52 | ||
|
|
afcae025ae | ||
|
|
1de36625a4 | ||
|
|
a2b6c7f5e6 | ||
|
|
799c7b077a | ||
|
|
cb5f944de6 | ||
|
|
23c4958145 | ||
|
|
edb2edc12c | ||
|
|
648a979a81 | ||
|
|
988de6de7b | ||
|
|
031abbfcb3 | ||
|
|
b59fcc26e5 | ||
|
|
acaa9381fe | ||
|
|
8d9e9260e6 | ||
|
|
0fc4a6daed | ||
|
|
af0c1d3af7 | ||
|
|
9ad3cd0ab9 | ||
|
|
00def272b0 | ||
|
|
383913505f | ||
|
|
ca8cb78c29 | ||
|
|
8821fb5dd0 | ||
|
|
3279a6ca53 | ||
|
|
6a1a98d73f | ||
|
|
1f067aad5b | ||
|
|
1388711105 | ||
|
|
618e5b4cc1 | ||
|
|
42c3ca5db5 | ||
|
|
534791776b | ||
|
|
0c6c53fc7d | ||
|
|
0dfd5ce07d | ||
|
|
2cd6d46f7c | ||
|
|
c333a9fadd | ||
|
|
ba3d1c66f0 | ||
|
|
7276e533ce | ||
|
|
8b84231042 | ||
|
|
77da744008 | ||
|
|
5da7a21119 | ||
|
|
78d742c712 | ||
|
|
1c97ea3e2c | ||
|
|
3d970defe9 | ||
|
|
6282794004 | ||
|
|
475c53a55d | ||
|
|
4547ff7b5d | ||
|
|
e7b4be3dc5 | ||
|
|
2bd85e04fc |
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Everything needs to be reviewed by Hank
|
||||
* @henrygd
|
||||
19
.github/DISCUSSION_TEMPLATE/ideas.yml
vendored
Normal file
19
.github/DISCUSSION_TEMPLATE/ideas.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
body:
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of Beszel is this about?
|
||||
options:
|
||||
- Hub
|
||||
- Agent
|
||||
- Hub & Agent
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please describe in detail what you want to share.
|
||||
validations:
|
||||
required: true
|
||||
68
.github/DISCUSSION_TEMPLATE/support.yml
vendored
68
.github/DISCUSSION_TEMPLATE/support.yml
vendored
@@ -1,19 +1,54 @@
|
||||
body:
|
||||
- type: markdown
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
value: |
|
||||
### Before opening a discussion:
|
||||
label: Welcome!
|
||||
description: |
|
||||
Thank you for reaching out to the Beszel community for support! To help us assist you better, please make sure to review the following points before submitting your request:
|
||||
|
||||
- Check the [common issues guide](https://beszel.dev/guide/common-issues).
|
||||
- Search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
Please note:
|
||||
- For translation-related issues or requests, please use the [Crowdin project](https://crowdin.com/project/beszel).
|
||||
**- Please do not submit support reqeusts that are specific to ZFS. We plan to add integration with ZFS utilities in the near future.**
|
||||
|
||||
options:
|
||||
- label: I have read the [Documentation](https://beszel.dev/guide/getting-started)
|
||||
required: true
|
||||
- label: I have checked the [Common Issues Guide](https://beszel.dev/guide/common-issues) and my problem was not mentioned there.
|
||||
required: true
|
||||
- label: I have searched open and closed issues and discussions and my problem was not mentioned before.
|
||||
required: true
|
||||
- label: I have verified I am using the latest version available. You can check the latest release [here](https://github.com/henrygd/beszel/releases).
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of Beszel is this about?
|
||||
options:
|
||||
- Hub
|
||||
- Agent
|
||||
- Hub & Agent
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the issue or question. If applicable, add screenshots to help explain your problem.
|
||||
label: Problem Description
|
||||
description: |
|
||||
How to write a good bug report?
|
||||
|
||||
- Respect the issue template as much as possible.
|
||||
- The title should be short and descriptive.
|
||||
- Explain the conditions which led you to report this issue: the context.
|
||||
- The context should lead to something, a problem that you’re facing.
|
||||
- Remain clear and concise.
|
||||
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
@@ -21,13 +56,15 @@ body:
|
||||
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Beszel version
|
||||
placeholder: 0.9.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# - type: input
|
||||
# id: version
|
||||
# attributes:
|
||||
# label: Beszel version
|
||||
# placeholder: 0.9.1
|
||||
# validations:
|
||||
# required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
@@ -41,18 +78,21 @@ body:
|
||||
- Other (please describe above)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: Please provide any relevant service configuration
|
||||
render: yaml
|
||||
|
||||
- type: textarea
|
||||
id: hub-logs
|
||||
attributes:
|
||||
label: Hub Logs
|
||||
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
||||
render: json
|
||||
|
||||
- type: textarea
|
||||
id: agent-logs
|
||||
attributes:
|
||||
|
||||
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,30 @@
|
||||
name: 🐛 Bug report
|
||||
description: Report a new bug or issue.
|
||||
description: Use this template to report a bug or issue.
|
||||
title: '[Bug]: '
|
||||
labels: ['bug', "needs confirmation"]
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Welcome!
|
||||
description: |
|
||||
The issue tracker is for reporting bugs and feature requests only. For end-user related support questions, please use the **[GitHub Discussions](https://github.com/henrygd/beszel/discussions/new?category=support)** instead
|
||||
|
||||
Please note:
|
||||
- For translation-related issues or requests, please use the [Crowdin project](https://crowdin.com/project/beszel).
|
||||
- To request a change or feature, use the [feature request form](https://github.com/henrygd/beszel/issues/new?template=feature_request.yml).
|
||||
- Any issues that can be resolved by consulting the documentation or by reviewing existing open or closed issues will be closed.
|
||||
**- Please do not submit bugs that are specific to ZFS. We plan to add integration with ZFS utilities in the near future.**
|
||||
|
||||
options:
|
||||
- label: I have read the [Documentation](https://beszel.dev/guide/getting-started)
|
||||
required: true
|
||||
- label: I have checked the [Common Issues Guide](https://beszel.dev/guide/common-issues) and my problem was not mentioned there.
|
||||
required: true
|
||||
- label: I have searched open and closed issues and my problem was not mentioned before.
|
||||
required: true
|
||||
- label: I have verified I am using the latest version available. You can check the latest release [here](https://github.com/henrygd/beszel/releases).
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
@@ -12,81 +34,53 @@ body:
|
||||
- Hub
|
||||
- Agent
|
||||
- Hub & Agent
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- For more general support, please [start a support thread](https://github.com/henrygd/beszel/discussions/new?category=support).
|
||||
- To request a change or feature, use the [feature request form](https://github.com/henrygd/beszel/issues/new?template=feature_request.yml).
|
||||
- Please do not submit bugs that are specific to ZFS. We plan to add integration with ZFS utilities in the near future.
|
||||
|
||||
### Before submitting a bug report:
|
||||
|
||||
- Check the [common issues guide](https://beszel.dev/guide/common-issues).
|
||||
- Search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Explain the issue you experienced clearly and concisely.
|
||||
placeholder: I went to the coffee pot and it was empty.
|
||||
label: Problem Description
|
||||
description: |
|
||||
How to write a good bug report?
|
||||
|
||||
- Respect the issue template as much as possible.
|
||||
- The title should be short and descriptive.
|
||||
- Explain the conditions which led you to report this issue: the context.
|
||||
- The context should lead to something, a problem that you’re facing.
|
||||
- Remain clear and concise.
|
||||
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: In a perfect world, what should have happened?
|
||||
description: |
|
||||
In a perfect world, what should have happened?
|
||||
**Important:** Be specific. Vague descriptions like "it should work" are not helpful.
|
||||
placeholder: When I got to the coffee pot, it should have been full.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Describe how to reproduce the issue in repeatable steps.
|
||||
description: |
|
||||
Provide detailed, numbered steps that someone else can follow to reproduce the issue.
|
||||
**Important:** Vague descriptions like "it doesn't work" or "it's broken" will result in the issue being closed.
|
||||
Include specific actions, URLs, button clicks, and any relevant data or configuration.
|
||||
placeholder: |
|
||||
1. Go to the coffee pot.
|
||||
2. Make more coffee.
|
||||
3. Pour it into a cup.
|
||||
4. Observe that the cup is empty instead of full.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: Which category does this relate to most?
|
||||
options:
|
||||
- Metrics
|
||||
- Charts & Visualization
|
||||
- Settings & Configuration
|
||||
- Notifications & Alerts
|
||||
- Authentication
|
||||
- Installation
|
||||
- Performance
|
||||
- UI / UX
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: metrics
|
||||
attributes:
|
||||
label: Affected Metrics
|
||||
description: If applicable, which specific metric does this relate to most?
|
||||
options:
|
||||
- CPU
|
||||
- Memory
|
||||
- Storage
|
||||
- Network
|
||||
- Containers
|
||||
- GPU
|
||||
- Sensors
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
@@ -94,6 +88,7 @@ body:
|
||||
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
@@ -101,6 +96,7 @@ body:
|
||||
placeholder: 0.9.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
@@ -114,18 +110,21 @@ body:
|
||||
- Other (please describe above)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: Please provide any relevant service configuration
|
||||
render: yaml
|
||||
|
||||
- type: textarea
|
||||
id: hub-logs
|
||||
attributes:
|
||||
label: Hub Logs
|
||||
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
||||
render: json
|
||||
|
||||
- type: textarea
|
||||
id: agent-logs
|
||||
attributes:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🗣️ Translations
|
||||
url: https://crowdin.com/project/beszel
|
||||
about: Please report translation issues and request new translations here.
|
||||
- name: 💬 Support and questions
|
||||
url: https://github.com/henrygd/beszel/discussions
|
||||
about: Ask and answer questions here.
|
||||
|
||||
81
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
81
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,8 +1,25 @@
|
||||
name: 🚀 Feature request
|
||||
description: Request a new feature or change.
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement", "needs review"]
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Welcome!
|
||||
description: |
|
||||
The issue tracker is for reporting bugs and feature requests only. For end-user related support questions, please use the **[GitHub Discussions](https://github.com/henrygd/beszel/discussions)** instead
|
||||
|
||||
Please note:
|
||||
- For **Bug reports**, use the [Bug Form](https://github.com/henrygd/beszel/issues/new?template=bug_report.yml).
|
||||
- Any requests for new translations should be requested within the [crowdin project](https://crowdin.com/project/beszel).
|
||||
- Create one issue per feature request. This helps us keep track of requests and prioritize them accordingly.
|
||||
|
||||
options:
|
||||
- label: I have searched open and closed feature requests to make sure this or similar feature request does not already exist.
|
||||
required: true
|
||||
- label: This is a feature request, not a bug report or support question.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
@@ -12,65 +29,29 @@ body:
|
||||
- Hub
|
||||
- Agent
|
||||
- Hub & Agent
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting, please search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the feature you would like to see
|
||||
label: Description
|
||||
description: |
|
||||
Describe the solution or feature you'd like. Explain what problem this solves or what value it adds.
|
||||
**Important:** Be specific and detailed. Vague requests like "make it better" will be closed.
|
||||
placeholder: |
|
||||
Example:
|
||||
- What is the feature?
|
||||
- What problem does it solve?
|
||||
- How should it work?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: Motivation / Use Case
|
||||
description: Why do you want this feature? What problem does it solve?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe how you would like to see this feature implemented
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please attach any relevant screenshots, such as images from your current solution or similar implementations.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: Which category does this relate to most?
|
||||
options:
|
||||
- Metrics
|
||||
- Charts & Visualization
|
||||
- Settings & Configuration
|
||||
- Notifications & Alerts
|
||||
- Authentication
|
||||
- Installation
|
||||
- Performance
|
||||
- UI / UX
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: metrics
|
||||
attributes:
|
||||
label: Affected Metrics
|
||||
description: If applicable, which specific metric does this relate to most?
|
||||
options:
|
||||
- CPU
|
||||
- Memory
|
||||
- Storage
|
||||
- Network
|
||||
- Containers
|
||||
- GPU
|
||||
- Sensors
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
3
.github/workflows/inactivity-actions.yml
vendored
3
.github/workflows/inactivity-actions.yml
vendored
@@ -51,7 +51,8 @@ jobs:
|
||||
# Labels
|
||||
stale-issue-label: 'stale'
|
||||
remove-stale-when-updated: true
|
||||
only-issue-labels: 'awaiting-requester'
|
||||
any-of-labels: 'awaiting-requester'
|
||||
exempt-issue-labels: 'enhancement'
|
||||
|
||||
# Exemptions
|
||||
exempt-assignees: true
|
||||
|
||||
82
.github/workflows/label-from-dropdown.yml
vendored
82
.github/workflows/label-from-dropdown.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: Label issues from dropdowns
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
label_from_dropdown:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Apply labels based on dropdown choices
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
|
||||
const issueNumber = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Get the issue body
|
||||
const body = context.payload.issue.body;
|
||||
|
||||
// Helper to find dropdown value in the body (assuming markdown format)
|
||||
function extractSectionValue(heading) {
|
||||
const regex = new RegExp(`### ${heading}\\s+([\\s\\S]*?)(?:\\n###|$)`, 'i');
|
||||
const match = body.match(regex);
|
||||
if (match) {
|
||||
// Get the first non-empty line after the heading
|
||||
const lines = match[1].split('\n').map(l => l.trim()).filter(Boolean);
|
||||
return lines[0] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract dropdown selections
|
||||
const category = extractSectionValue('Category');
|
||||
const metrics = extractSectionValue('Affected Metrics');
|
||||
const component = extractSectionValue('Component');
|
||||
|
||||
// Build labels to add
|
||||
let labelsToAdd = [];
|
||||
if (category) labelsToAdd.push(category);
|
||||
if (metrics) labelsToAdd.push(metrics);
|
||||
if (component) labelsToAdd.push(component);
|
||||
|
||||
// Get existing labels in the repo
|
||||
const { data: existingLabels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100
|
||||
});
|
||||
const existingLabelNames = existingLabels.map(l => l.name);
|
||||
|
||||
// Find labels that need to be created
|
||||
const labelsToCreate = labelsToAdd.filter(label => !existingLabelNames.includes(label));
|
||||
|
||||
// Create missing labels (with a default color)
|
||||
for (const label of labelsToCreate) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: label,
|
||||
color: 'ededed' // light gray, you can pick any hex color
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore if label already exists (race condition), otherwise rethrow
|
||||
if (!e || e.status !== 422) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply all labels (they all exist now)
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: labelsToAdd
|
||||
});
|
||||
}
|
||||
@@ -76,6 +76,18 @@ builds:
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
|
||||
- id: beszel-agent-linux-amd64-glibc
|
||||
binary: beszel-agent
|
||||
main: internal/cmd/agent/agent.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -tags=glibc
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
|
||||
archives:
|
||||
- id: beszel-agent
|
||||
formats: [tar.gz]
|
||||
@@ -89,6 +101,15 @@ archives:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
|
||||
- id: beszel-agent-linux-amd64-glibc
|
||||
formats: [tar.gz]
|
||||
ids:
|
||||
- beszel-agent-linux-amd64-glibc
|
||||
name_template: >-
|
||||
{{ .Binary }}_
|
||||
{{- .Os }}_
|
||||
{{- .Arch }}_glibc
|
||||
|
||||
- id: beszel
|
||||
formats: [tar.gz]
|
||||
ids:
|
||||
@@ -137,9 +158,7 @@ nfpms:
|
||||
- debconf
|
||||
scripts:
|
||||
templates: ./supplemental/debian/templates
|
||||
# Currently broken due to a bug in goreleaser
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
#config: ./supplemental/debian/config.sh
|
||||
config: ./supplemental/debian/config.sh
|
||||
|
||||
scoops:
|
||||
- ids: [beszel-agent]
|
||||
|
||||
@@ -65,7 +65,7 @@ func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
|
||||
continue
|
||||
}
|
||||
totalCapacity += bat.Full
|
||||
totalCharge += bat.Current
|
||||
totalCharge += min(bat.Current, bat.Full)
|
||||
if bat.State.Raw >= 0 {
|
||||
batteryState = uint8(bat.State.Raw)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ 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/henrygd/beszel/internal/entities/systemd"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/lxzan/gws"
|
||||
@@ -259,40 +256,16 @@ func (client *WebSocketClient) sendMessage(data any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// sendResponse sends a response with optional request ID for the new protocol
|
||||
// sendResponse sends a response with optional request ID.
|
||||
// For ID-based requests, we must populate legacy typed fields for backward
|
||||
// compatibility with older hubs (<= 0.17) that don't read the generic Data field.
|
||||
func (client *WebSocketClient) sendResponse(data any, requestID *uint32) error {
|
||||
if requestID != nil {
|
||||
// New format with ID - use typed fields
|
||||
response := common.AgentResponse{
|
||||
Id: requestID,
|
||||
}
|
||||
|
||||
// Set the appropriate typed field based on data type
|
||||
switch v := data.(type) {
|
||||
case *system.CombinedData:
|
||||
response.SystemData = v
|
||||
case *common.FingerprintResponse:
|
||||
response.Fingerprint = v
|
||||
case string:
|
||||
response.String = &v
|
||||
case map[string]smart.SmartData:
|
||||
response.SmartData = v
|
||||
case systemd.ServiceDetails:
|
||||
response.ServiceInfo = v
|
||||
// case []byte:
|
||||
// response.RawBytes = v
|
||||
// case string:
|
||||
// response.RawBytes = []byte(v)
|
||||
default:
|
||||
// For any other type, convert to error
|
||||
response.Error = fmt.Sprintf("unsupported response type: %T", data)
|
||||
}
|
||||
|
||||
response := newAgentResponse(data, requestID)
|
||||
return client.sendMessage(response)
|
||||
} else {
|
||||
// Legacy format - send data directly
|
||||
return client.sendMessage(data)
|
||||
}
|
||||
// Legacy format - send data directly
|
||||
return client.sendMessage(data)
|
||||
}
|
||||
|
||||
// getUserAgent returns one of two User-Agent strings based on current time.
|
||||
|
||||
@@ -694,7 +694,8 @@ func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (strin
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
if err := decodeDockerLogStream(resp.Body, &builder); err != nil {
|
||||
multiplexed := resp.Header.Get("Content-Type") == "application/vnd.docker.multiplexed-stream"
|
||||
if err := decodeDockerLogStream(resp.Body, &builder, multiplexed); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -706,7 +707,11 @@ func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (strin
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error {
|
||||
func decodeDockerLogStream(reader io.Reader, builder *strings.Builder, multiplexed bool) error {
|
||||
if !multiplexed {
|
||||
_, err := io.Copy(builder, io.LimitReader(reader, maxTotalLogSize))
|
||||
return err
|
||||
}
|
||||
const headerSize = 8
|
||||
var header [headerSize]byte
|
||||
totalBytesRead := 0
|
||||
|
||||
@@ -950,6 +950,7 @@ func TestDecodeDockerLogStream(t *testing.T) {
|
||||
input []byte
|
||||
expected string
|
||||
expectError bool
|
||||
multiplexed bool
|
||||
}{
|
||||
{
|
||||
name: "simple log entry",
|
||||
@@ -960,6 +961,7 @@ func TestDecodeDockerLogStream(t *testing.T) {
|
||||
},
|
||||
expected: "Hello World",
|
||||
expectError: false,
|
||||
multiplexed: true,
|
||||
},
|
||||
{
|
||||
name: "multiple frames",
|
||||
@@ -973,6 +975,7 @@ func TestDecodeDockerLogStream(t *testing.T) {
|
||||
},
|
||||
expected: "HelloWorld",
|
||||
expectError: false,
|
||||
multiplexed: true,
|
||||
},
|
||||
{
|
||||
name: "zero length frame",
|
||||
@@ -985,12 +988,20 @@ func TestDecodeDockerLogStream(t *testing.T) {
|
||||
},
|
||||
expected: "Hello",
|
||||
expectError: false,
|
||||
multiplexed: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: []byte{},
|
||||
expected: "",
|
||||
expectError: false,
|
||||
multiplexed: true,
|
||||
},
|
||||
{
|
||||
name: "raw stream (not multiplexed)",
|
||||
input: []byte("raw log content"),
|
||||
expected: "raw log content",
|
||||
multiplexed: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -998,7 +1009,7 @@ func TestDecodeDockerLogStream(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := bytes.NewReader(tt.input)
|
||||
var builder strings.Builder
|
||||
err := decodeDockerLogStream(reader, &builder)
|
||||
err := decodeDockerLogStream(reader, &builder, tt.multiplexed)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
@@ -1022,7 +1033,7 @@ func TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {
|
||||
|
||||
reader := bytes.NewReader(input)
|
||||
var builder strings.Builder
|
||||
err := decodeDockerLogStream(reader, &builder)
|
||||
err := decodeDockerLogStream(reader, &builder, true)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "log frame size")
|
||||
@@ -1056,7 +1067,7 @@ func TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {
|
||||
|
||||
reader := bytes.NewReader(input)
|
||||
var builder strings.Builder
|
||||
err := decodeDockerLogStream(reader, &builder)
|
||||
err := decodeDockerLogStream(reader, &builder, true)
|
||||
|
||||
// Should complete without error (graceful truncation)
|
||||
assert.NoError(t, err)
|
||||
|
||||
47
agent/gpu.go
47
agent/gpu.go
@@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
@@ -14,14 +15,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
// Commands
|
||||
nvidiaSmiCmd string = "nvidia-smi"
|
||||
rocmSmiCmd string = "rocm-smi"
|
||||
amdgpuCmd string = "amdgpu" // internal cmd for sysfs collection
|
||||
tegraStatsCmd string = "tegrastats"
|
||||
|
||||
// Polling intervals
|
||||
@@ -42,6 +42,7 @@ type GPUManager struct {
|
||||
sync.Mutex
|
||||
nvidiaSmi bool
|
||||
rocmSmi bool
|
||||
amdgpu bool
|
||||
tegrastats bool
|
||||
intelGpuStats bool
|
||||
nvml bool
|
||||
@@ -137,10 +138,10 @@ func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
|
||||
// use closure to avoid recompiling the regex
|
||||
ramPattern := regexp.MustCompile(`RAM (\d+)/(\d+)MB`)
|
||||
gr3dPattern := regexp.MustCompile(`GR3D_FREQ (\d+)%`)
|
||||
tempPattern := regexp.MustCompile(`tj@(\d+\.?\d*)C`)
|
||||
tempPattern := regexp.MustCompile(`(?:tj|GPU)@(\d+\.?\d*)C`)
|
||||
// Orin Nano / NX do not have GPU specific power monitor
|
||||
// TODO: Maybe use VDD_IN for Nano / NX and add a total system power chart
|
||||
powerPattern := regexp.MustCompile(`(GPU_SOC|CPU_GPU_CV) (\d+)mW`)
|
||||
powerPattern := regexp.MustCompile(`(GPU_SOC|CPU_GPU_CV)\s+(\d+)mW|VDD_SYS_GPU\s+(\d+)/\d+`)
|
||||
|
||||
// jetson devices have only one gpu so we'll just initialize here
|
||||
gpuData := &system.GPUData{Name: "GPU"}
|
||||
@@ -169,7 +170,13 @@ func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
|
||||
// Parse power usage
|
||||
powerMatches := powerPattern.FindSubmatch(output)
|
||||
if powerMatches != nil {
|
||||
power, _ := strconv.ParseFloat(string(powerMatches[2]), 64)
|
||||
// powerMatches[2] is the "(GPU_SOC|CPU_GPU_CV) <N>mW" capture
|
||||
// powerMatches[3] is the "VDD_SYS_GPU <N>/<N>" capture
|
||||
powerStr := string(powerMatches[2])
|
||||
if powerStr == "" {
|
||||
powerStr = string(powerMatches[3])
|
||||
}
|
||||
power, _ := strconv.ParseFloat(powerStr, 64)
|
||||
gpuData.Power += power / milliwattsInAWatt
|
||||
}
|
||||
gpuData.Count++
|
||||
@@ -232,10 +239,11 @@ func (gm *GPUManager) parseAmdData(output []byte) bool {
|
||||
totalMemory, _ := strconv.ParseFloat(v.MemoryTotal, 64)
|
||||
usage, _ := strconv.ParseFloat(v.Usage, 64)
|
||||
|
||||
if _, ok := gm.GpuDataMap[v.ID]; !ok {
|
||||
gm.GpuDataMap[v.ID] = &system.GPUData{Name: v.Name}
|
||||
id := v.ID
|
||||
if _, ok := gm.GpuDataMap[id]; !ok {
|
||||
gm.GpuDataMap[id] = &system.GPUData{Name: v.Name}
|
||||
}
|
||||
gpu := gm.GpuDataMap[v.ID]
|
||||
gpu := gm.GpuDataMap[id]
|
||||
gpu.Temperature, _ = strconv.ParseFloat(v.Temperature, 64)
|
||||
gpu.MemoryUsed = bytesToMegabytes(memoryUsage)
|
||||
gpu.MemoryTotal = bytesToMegabytes(totalMemory)
|
||||
@@ -393,7 +401,13 @@ func (gm *GPUManager) detectGPUs() error {
|
||||
gm.nvidiaSmi = true
|
||||
}
|
||||
if _, err := exec.LookPath(rocmSmiCmd); err == nil {
|
||||
gm.rocmSmi = true
|
||||
if val, _ := GetEnv("AMD_SYSFS"); val == "true" {
|
||||
gm.amdgpu = true
|
||||
} else {
|
||||
gm.rocmSmi = true
|
||||
}
|
||||
} else if gm.hasAmdSysfs() {
|
||||
gm.amdgpu = true
|
||||
}
|
||||
if _, err := exec.LookPath(tegraStatsCmd); err == nil {
|
||||
gm.tegrastats = true
|
||||
@@ -402,10 +416,10 @@ func (gm *GPUManager) detectGPUs() error {
|
||||
if _, err := exec.LookPath(intelGpuStatsCmd); err == nil {
|
||||
gm.intelGpuStats = true
|
||||
}
|
||||
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats || gm.intelGpuStats || gm.nvml {
|
||||
if gm.nvidiaSmi || gm.rocmSmi || gm.amdgpu || gm.tegrastats || gm.intelGpuStats || gm.nvml {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, tegrastats, or intel_gpu_top")
|
||||
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, or intel_gpu_top")
|
||||
}
|
||||
|
||||
// startCollector starts the appropriate GPU data collector based on the command
|
||||
@@ -442,6 +456,12 @@ func (gm *GPUManager) startCollector(command string) {
|
||||
collector.cmdArgs = []string{"--interval", tegraStatsInterval}
|
||||
collector.parse = gm.getJetsonParser()
|
||||
go collector.start()
|
||||
case amdgpuCmd:
|
||||
go func() {
|
||||
if err := gm.collectAmdStats(); err != nil {
|
||||
slog.Warn("Error collecting AMD GPU data via sysfs", "err", err)
|
||||
}
|
||||
}()
|
||||
case rocmSmiCmd:
|
||||
collector.cmdArgs = []string{"--showid", "--showtemp", "--showuse", "--showpower", "--showproductname", "--showmeminfo", "vram", "--json"}
|
||||
collector.parse = gm.parseAmdData
|
||||
@@ -453,7 +473,7 @@ func (gm *GPUManager) startCollector(command string) {
|
||||
if failures > maxFailureRetries {
|
||||
break
|
||||
}
|
||||
slog.Warn("Error collecting AMD GPU data", "err", err)
|
||||
slog.Warn("Error collecting AMD GPU data via rocm-smi", "err", err)
|
||||
}
|
||||
time.Sleep(rocmSmiInterval)
|
||||
}
|
||||
@@ -491,6 +511,9 @@ func NewGPUManager() (*GPUManager, error) {
|
||||
if gm.rocmSmi {
|
||||
gm.startCollector(rocmSmiCmd)
|
||||
}
|
||||
if gm.amdgpu {
|
||||
gm.startCollector(amdgpuCmd)
|
||||
}
|
||||
if gm.tegrastats {
|
||||
gm.startCollector(tegraStatsCmd)
|
||||
}
|
||||
|
||||
184
agent/gpu_amd_linux.go
Normal file
184
agent/gpu_amd_linux.go
Normal file
@@ -0,0 +1,184 @@
|
||||
//go:build linux
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
// hasAmdSysfs returns true if any AMD GPU sysfs nodes are found
|
||||
func (gm *GPUManager) hasAmdSysfs() bool {
|
||||
cards, err := filepath.Glob("/sys/class/drm/card*/device/vendor")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, vendorPath := range cards {
|
||||
vendor, err := os.ReadFile(vendorPath)
|
||||
if err == nil && strings.TrimSpace(string(vendor)) == "0x1002" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// collectAmdStats collects AMD GPU metrics directly from sysfs to avoid the overhead of rocm-smi
|
||||
func (gm *GPUManager) collectAmdStats() error {
|
||||
cards, err := filepath.Glob("/sys/class/drm/card*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var amdGpuPaths []string
|
||||
for _, card := range cards {
|
||||
// Ignore symbolic links and non-main card directories
|
||||
if strings.Contains(filepath.Base(card), "-") || !isAmdGpu(card) {
|
||||
continue
|
||||
}
|
||||
amdGpuPaths = append(amdGpuPaths, card)
|
||||
}
|
||||
|
||||
if len(amdGpuPaths) == 0 {
|
||||
return errNoValidData
|
||||
}
|
||||
|
||||
slog.Debug("Using sysfs for AMD GPU data collection")
|
||||
|
||||
failures := 0
|
||||
for {
|
||||
hasData := false
|
||||
for _, cardPath := range amdGpuPaths {
|
||||
if gm.updateAmdGpuData(cardPath) {
|
||||
hasData = true
|
||||
}
|
||||
}
|
||||
if !hasData {
|
||||
failures++
|
||||
if failures > maxFailureRetries {
|
||||
return errNoValidData
|
||||
}
|
||||
slog.Warn("No AMD GPU data from sysfs", "failures", failures)
|
||||
time.Sleep(retryWaitTime)
|
||||
continue
|
||||
}
|
||||
failures = 0
|
||||
time.Sleep(rocmSmiInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func isAmdGpu(cardPath string) bool {
|
||||
vendorPath := filepath.Join(cardPath, "device/vendor")
|
||||
vendor, err := os.ReadFile(vendorPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(vendor)) == "0x1002"
|
||||
}
|
||||
|
||||
// updateAmdGpuData reads GPU metrics from sysfs and updates the GPU data map.
|
||||
// Returns true if at least some data was successfully read.
|
||||
func (gm *GPUManager) updateAmdGpuData(cardPath string) bool {
|
||||
devicePath := filepath.Join(cardPath, "device")
|
||||
id := filepath.Base(cardPath)
|
||||
|
||||
// Read all sysfs values first (no lock needed - these can be slow)
|
||||
usage, usageErr := readSysfsFloat(filepath.Join(devicePath, "gpu_busy_percent"))
|
||||
memUsed, memUsedErr := readSysfsFloat(filepath.Join(devicePath, "mem_info_vram_used"))
|
||||
memTotal, _ := readSysfsFloat(filepath.Join(devicePath, "mem_info_vram_total"))
|
||||
|
||||
var temp, power float64
|
||||
hwmons, _ := filepath.Glob(filepath.Join(devicePath, "hwmon/hwmon*"))
|
||||
for _, hwmonDir := range hwmons {
|
||||
if t, err := readSysfsFloat(filepath.Join(hwmonDir, "temp1_input")); err == nil {
|
||||
temp = t / 1000.0
|
||||
}
|
||||
if p, err := readSysfsFloat(filepath.Join(hwmonDir, "power1_average")); err == nil {
|
||||
power += p / 1000000.0
|
||||
} else if p, err := readSysfsFloat(filepath.Join(hwmonDir, "power1_input")); err == nil {
|
||||
power += p / 1000000.0
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we got any meaningful data
|
||||
if usageErr != nil && memUsedErr != nil && temp == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Single lock to update all values atomically
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
|
||||
gpu, ok := gm.GpuDataMap[id]
|
||||
if !ok {
|
||||
gpu = &system.GPUData{Name: getAmdGpuName(devicePath)}
|
||||
gm.GpuDataMap[id] = gpu
|
||||
}
|
||||
|
||||
if usageErr == nil {
|
||||
gpu.Usage += usage
|
||||
}
|
||||
gpu.MemoryUsed = bytesToMegabytes(memUsed)
|
||||
gpu.MemoryTotal = bytesToMegabytes(memTotal)
|
||||
gpu.Temperature = temp
|
||||
gpu.Power += power
|
||||
gpu.Count++
|
||||
return true
|
||||
}
|
||||
|
||||
func readSysfsFloat(path string) (float64, error) {
|
||||
val, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.ParseFloat(strings.TrimSpace(string(val)), 64)
|
||||
}
|
||||
|
||||
// getAmdGpuName attempts to get a descriptive GPU name.
|
||||
// First tries product_name (rarely available), then looks up the PCI device ID.
|
||||
// Falls back to showing the raw device ID if not found in the lookup table.
|
||||
func getAmdGpuName(devicePath string) string {
|
||||
// Try product_name first (works for some enterprise GPUs)
|
||||
if prod, err := os.ReadFile(filepath.Join(devicePath, "product_name")); err == nil {
|
||||
return strings.TrimSpace(string(prod))
|
||||
}
|
||||
|
||||
// Read PCI device ID and look it up
|
||||
if deviceID, err := os.ReadFile(filepath.Join(devicePath, "device")); err == nil {
|
||||
id := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(string(deviceID))), "0x")
|
||||
if name, ok := getRadeonNames()[id]; ok {
|
||||
return fmt.Sprintf("Radeon %s", name)
|
||||
}
|
||||
return fmt.Sprintf("AMD GPU (%s)", id)
|
||||
}
|
||||
|
||||
return "AMD GPU"
|
||||
}
|
||||
|
||||
// getRadeonNames returns the AMD GPU name lookup table
|
||||
// Device IDs from https://pci-ids.ucw.cz/read/PC/1002
|
||||
var getRadeonNames = sync.OnceValue(func() map[string]string {
|
||||
return map[string]string{
|
||||
"7550": "RX 9070",
|
||||
"7590": "RX 9060 XT",
|
||||
"7551": "AI PRO R9700",
|
||||
|
||||
"744c": "RX 7900",
|
||||
|
||||
"1681": "680M",
|
||||
|
||||
"7448": "PRO W7900",
|
||||
"745e": "PRO W7800",
|
||||
"7470": "PRO W7700",
|
||||
"73e3": "PRO W6600",
|
||||
"7422": "PRO W6400",
|
||||
"7341": "PRO W5500",
|
||||
}
|
||||
})
|
||||
15
agent/gpu_amd_unsupported.go
Normal file
15
agent/gpu_amd_unsupported.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !linux
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func (gm *GPUManager) hasAmdSysfs() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (gm *GPUManager) collectAmdStats() error {
|
||||
return errors.ErrUnsupported
|
||||
}
|
||||
@@ -27,10 +27,11 @@ func (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {
|
||||
defer gm.Unlock()
|
||||
|
||||
// only one gpu for now - cmd doesn't provide all by default
|
||||
gpuData, ok := gm.GpuDataMap["0"]
|
||||
id := "i0" // prefix with i to avoid conflicts with nvidia card ids
|
||||
gpuData, ok := gm.GpuDataMap[id]
|
||||
if !ok {
|
||||
gpuData = &system.GPUData{Name: "GPU", Engines: make(map[string]float64)}
|
||||
gm.GpuDataMap["0"] = gpuData
|
||||
gm.GpuDataMap[id] = gpuData
|
||||
}
|
||||
|
||||
gpuData.Power += sample.PowerGPU
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//go:build amd64 && (windows || (linux && glibc))
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ebitengine/purego"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// NVML constants and types
|
||||
@@ -72,7 +74,7 @@ type nvmlCollector struct {
|
||||
}
|
||||
|
||||
func (c *nvmlCollector) init() error {
|
||||
slog.Info("NVML: Initializing")
|
||||
slog.Debug("NVML: Initializing")
|
||||
libPath := getNVMLPath()
|
||||
|
||||
lib, err := openLibrary(libPath)
|
||||
@@ -159,7 +161,7 @@ func (c *nvmlCollector) collect() {
|
||||
gpu := c.gm.GpuDataMap[id]
|
||||
|
||||
if bdf != "" && !c.isGPUActive(bdf) {
|
||||
slog.Info("NVML: GPU is suspended, skipping", "bdf", bdf)
|
||||
slog.Debug("NVML: GPU is suspended, skipping", "bdf", bdf)
|
||||
gpu.Temperature = 0
|
||||
gpu.MemoryUsed = 0
|
||||
continue
|
||||
@@ -168,31 +170,45 @@ func (c *nvmlCollector) collect() {
|
||||
// Utilization
|
||||
var utilization nvmlUtilization
|
||||
if ret := nvmlDeviceGetUtilizationRates(device, &utilization); ret != nvmlReturn(nvmlSuccess) {
|
||||
slog.Info("NVML: Utilization failed (GPU likely suspended)", "bdf", bdf, "ret", ret)
|
||||
slog.Debug("NVML: Utilization failed (GPU likely suspended)", "bdf", bdf, "ret", ret)
|
||||
gpu.Temperature = 0
|
||||
gpu.MemoryUsed = 0
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("NVML: Collecting data for GPU", "bdf", bdf)
|
||||
slog.Debug("NVML: Collecting data for GPU", "bdf", bdf)
|
||||
|
||||
// Temperature
|
||||
var temp uint32
|
||||
nvmlDeviceGetTemperature(device, 0, &temp) // 0 is NVML_TEMPERATURE_GPU
|
||||
|
||||
// Memory
|
||||
var usedMem, totalMem uint64
|
||||
if c.isV2 {
|
||||
var memory nvmlMemoryV2
|
||||
memory.Version = 0x02000028 // (2 << 24) | 40 bytes
|
||||
nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory)))
|
||||
usedMem = memory.Used
|
||||
totalMem = memory.Total
|
||||
// Memory: only poll if GPU is active to avoid leaving D3cold state (#1522)
|
||||
if utilization.Gpu > 0 {
|
||||
var usedMem, totalMem uint64
|
||||
if c.isV2 {
|
||||
var memory nvmlMemoryV2
|
||||
memory.Version = 0x02000028 // (2 << 24) | 40 bytes
|
||||
if ret := nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory))); ret != nvmlReturn(nvmlSuccess) {
|
||||
slog.Debug("NVML: MemoryInfo_v2 failed", "bdf", bdf, "ret", ret)
|
||||
} else {
|
||||
usedMem = memory.Used
|
||||
totalMem = memory.Total
|
||||
}
|
||||
} else {
|
||||
var memory nvmlMemoryV1
|
||||
if ret := nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory))); ret != nvmlReturn(nvmlSuccess) {
|
||||
slog.Debug("NVML: MemoryInfo failed", "bdf", bdf, "ret", ret)
|
||||
} else {
|
||||
usedMem = memory.Used
|
||||
totalMem = memory.Total
|
||||
}
|
||||
}
|
||||
if totalMem > 0 {
|
||||
gpu.MemoryUsed = float64(usedMem) / 1024 / 1024 / mebibytesInAMegabyte
|
||||
gpu.MemoryTotal = float64(totalMem) / 1024 / 1024 / mebibytesInAMegabyte
|
||||
}
|
||||
} else {
|
||||
var memory nvmlMemoryV1
|
||||
nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory)))
|
||||
usedMem = memory.Used
|
||||
totalMem = memory.Total
|
||||
slog.Debug("NVML: Skipping memory info (utilization=0)", "bdf", bdf)
|
||||
}
|
||||
|
||||
// Power
|
||||
@@ -200,11 +216,9 @@ func (c *nvmlCollector) collect() {
|
||||
nvmlDeviceGetPowerUsage(device, &power)
|
||||
|
||||
gpu.Temperature = float64(temp)
|
||||
gpu.MemoryUsed = float64(usedMem) / 1024 / 1024 / mebibytesInAMegabyte
|
||||
gpu.MemoryTotal = float64(totalMem) / 1024 / 1024 / mebibytesInAMegabyte
|
||||
gpu.Usage += float64(utilization.Gpu)
|
||||
gpu.Power += float64(power) / 1000.0
|
||||
gpu.Count++
|
||||
slog.Info("NVML: Collected data", "gpu", gpu)
|
||||
slog.Debug("NVML: Collected data", "gpu", gpu)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//go:build linux
|
||||
//go:build glibc && linux && amd64
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ebitengine/purego"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
func openLibrary(name string) (uintptr, error) {
|
||||
@@ -29,12 +29,12 @@ func (c *nvmlCollector) isGPUActive(bdf string) bool {
|
||||
statusPath := filepath.Join("/sys/bus/pci/devices", bdf, "power/runtime_status")
|
||||
status, err := os.ReadFile(statusPath)
|
||||
if err != nil {
|
||||
slog.Info("NVML: Can't read runtime_status", "bdf", bdf, "err", err)
|
||||
slog.Debug("NVML: Can't read runtime_status", "bdf", bdf, "err", err)
|
||||
return true // Assume active if we can't read status
|
||||
}
|
||||
statusStr := strings.TrimSpace(string(status))
|
||||
if statusStr != "active" && statusStr != "resuming" {
|
||||
slog.Info("NVML: GPU is not active", "bdf", bdf, "status", statusStr)
|
||||
slog.Debug("NVML: GPU not active", "bdf", bdf, "status", statusStr)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func (c *nvmlCollector) isGPUActive(bdf string) bool {
|
||||
if err == nil {
|
||||
pstateStr := strings.TrimSpace(string(pstate))
|
||||
if pstateStr != "D0" {
|
||||
slog.Debug("NVML: GPU not in D0 state", "bdf", bdf, "pstate", pstateStr)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
//go:build !linux && !windows
|
||||
//go:build (!linux && !windows) || !amd64 || (linux && !glibc)
|
||||
|
||||
package agent
|
||||
|
||||
import "fmt"
|
||||
|
||||
type nvmlCollector struct {
|
||||
gm *GPUManager
|
||||
}
|
||||
|
||||
func (c *nvmlCollector) init() error {
|
||||
return fmt.Errorf("nvml not supported on this platform")
|
||||
}
|
||||
|
||||
func (c *nvmlCollector) start() {}
|
||||
|
||||
func (c *nvmlCollector) collect() {}
|
||||
|
||||
func openLibrary(name string) (uintptr, error) {
|
||||
return 0, fmt.Errorf("nvml not supported on this platform")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build windows
|
||||
//go:build windows && amd64
|
||||
|
||||
package agent
|
||||
|
||||
|
||||
@@ -307,6 +307,19 @@ func TestParseJetsonData(t *testing.T) {
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "orin-style output with GPU@ temp and VDD_SYS_GPU power",
|
||||
input: "RAM 3276/7859MB (lfb 5x4MB) SWAP 1626/12122MB (cached 181MB) CPU [44%@1421,49%@2031,67%@2034,17%@1420,25%@1419,8%@1420] EMC_FREQ 1%@1866 GR3D_FREQ 0%@114 APE 150 MTS fg 1% bg 1% PLL@42.5C MCPU@42.5C PMIC@50C Tboard@38C GPU@39.5C BCPU@42.5C thermal@41.3C Tdiode@39.25C VDD_SYS_GPU 182/182 VDD_SYS_SOC 730/730 VDD_4V0_WIFI 0/0 VDD_IN 5297/5297 VDD_SYS_CPU 1917/1917 VDD_SYS_DDR 1241/1241",
|
||||
wantMetrics: &system.GPUData{
|
||||
Name: "GPU",
|
||||
MemoryUsed: 3276.0,
|
||||
MemoryTotal: 7859.0,
|
||||
Usage: 0.0,
|
||||
Power: 0.182, // 182mW -> 0.182W
|
||||
Temperature: 39.5,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -825,7 +838,7 @@ func TestInitializeSnapshots(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCalculateGPUAverage(t *testing.T) {
|
||||
t.Run("returns zero value when deltaCount is zero", func(t *testing.T) {
|
||||
t.Run("returns cached average when deltaCount is zero", func(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
lastSnapshots: map[uint16]map[string]*gpuSnapshot{
|
||||
5000: {
|
||||
@@ -838,9 +851,10 @@ func TestCalculateGPUAverage(t *testing.T) {
|
||||
}
|
||||
|
||||
gpu := &system.GPUData{
|
||||
Count: 10.0, // Same as snapshot, so delta = 0
|
||||
Usage: 100.0,
|
||||
Power: 200.0,
|
||||
Count: 10.0, // Same as snapshot, so delta = 0
|
||||
Usage: 100.0,
|
||||
Power: 200.0,
|
||||
Temperature: 50.0, // Non-zero to avoid "suspended" check
|
||||
}
|
||||
|
||||
result := gm.calculateGPUAverage("0", gpu, 5000)
|
||||
@@ -849,6 +863,31 @@ func TestCalculateGPUAverage(t *testing.T) {
|
||||
assert.Equal(t, 100.0, result.Power, "Should return cached average")
|
||||
})
|
||||
|
||||
t.Run("returns zero value when GPU is suspended", func(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
lastSnapshots: map[uint16]map[string]*gpuSnapshot{
|
||||
5000: {
|
||||
"0": {count: 10, usage: 100, power: 200},
|
||||
},
|
||||
},
|
||||
lastAvgData: map[string]system.GPUData{
|
||||
"0": {Usage: 50.0, Power: 100.0},
|
||||
},
|
||||
}
|
||||
|
||||
gpu := &system.GPUData{
|
||||
Name: "Test GPU",
|
||||
Count: 10.0,
|
||||
Temperature: 0,
|
||||
MemoryUsed: 0,
|
||||
}
|
||||
|
||||
result := gm.calculateGPUAverage("0", gpu, 5000)
|
||||
|
||||
assert.Equal(t, 0.0, result.Usage, "Should return zero usage")
|
||||
assert.Equal(t, 0.0, result.Power, "Should return zero power")
|
||||
})
|
||||
|
||||
t.Run("calculates average for standard GPU", func(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
lastSnapshots: map[uint16]map[string]*gpuSnapshot{
|
||||
@@ -1346,7 +1385,7 @@ func TestIntelUpdateFromStats(t *testing.T) {
|
||||
ok := gm.updateIntelFromStats(&sample1)
|
||||
assert.True(t, ok)
|
||||
|
||||
gpu := gm.GpuDataMap["0"]
|
||||
gpu := gm.GpuDataMap["i0"]
|
||||
require.NotNil(t, gpu)
|
||||
assert.Equal(t, "GPU", gpu.Name)
|
||||
assert.EqualValues(t, 10.5, gpu.Power)
|
||||
@@ -1368,7 +1407,7 @@ func TestIntelUpdateFromStats(t *testing.T) {
|
||||
ok = gm.updateIntelFromStats(&sample2)
|
||||
assert.True(t, ok)
|
||||
|
||||
gpu = gm.GpuDataMap["0"]
|
||||
gpu = gm.GpuDataMap["i0"]
|
||||
require.NotNil(t, gpu)
|
||||
assert.EqualValues(t, 10.5, gpu.Power)
|
||||
assert.EqualValues(t, 30.0, gpu.Engines["Render/3D"]) // 20 + 10
|
||||
@@ -1407,7 +1446,7 @@ echo "298 295 278 51 2.20 3.12 1675 942 5.75 1 2 9.50
|
||||
t.Fatalf("collectIntelStats error: %v", err)
|
||||
}
|
||||
|
||||
gpu := gm.GpuDataMap["0"]
|
||||
gpu := gm.GpuDataMap["i0"]
|
||||
require.NotNil(t, gpu)
|
||||
// Power should be sum of samples 2-4 (first is skipped): 2.0 + 1.8 + 2.2 = 6.0
|
||||
assert.EqualValues(t, 6.0, gpu.Power)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// HandlerContext provides context for request handlers
|
||||
|
||||
@@ -9,11 +9,31 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// healthFile is the path to the health file
|
||||
var healthFile = filepath.Join(os.TempDir(), "beszel_health")
|
||||
var healthFile = getHealthFilePath()
|
||||
|
||||
func getHealthFilePath() string {
|
||||
filename := "beszel_health"
|
||||
if runtime.GOOS == "linux" {
|
||||
fullPath := filepath.Join("/dev/shm", filename)
|
||||
if err := updateHealthFile(fullPath); err == nil {
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
return filepath.Join(os.TempDir(), filename)
|
||||
}
|
||||
|
||||
func updateHealthFile(path string) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
// Check checks if the agent is connected by checking the modification time of the health file
|
||||
func Check() error {
|
||||
@@ -30,11 +50,7 @@ func Check() error {
|
||||
|
||||
// Update updates the modification time of the health file
|
||||
func Update() error {
|
||||
file, err := os.Create(healthFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
return updateHealthFile(healthFile)
|
||||
}
|
||||
|
||||
// CleanUp removes the health file
|
||||
|
||||
@@ -52,7 +52,12 @@ class Program
|
||||
foreach (var sensor in hardware.Sensors)
|
||||
{
|
||||
var validTemp = sensor.SensorType == SensorType.Temperature && sensor.Value.HasValue;
|
||||
if (!validTemp || sensor.Name.Contains("Distance"))
|
||||
if (!validTemp ||
|
||||
sensor.Name.IndexOf("Distance", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sensor.Name.IndexOf("Limit", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sensor.Name.IndexOf("Critical", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sensor.Name.IndexOf("Warning", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sensor.Name.IndexOf("Resolution", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.4" />
|
||||
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
31
agent/response.go
Normal file
31
agent/response.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
)
|
||||
|
||||
// newAgentResponse creates an AgentResponse using legacy typed fields.
|
||||
// This maintains backward compatibility with <= 0.17 hubs that expect specific fields.
|
||||
func newAgentResponse(data any, requestID *uint32) common.AgentResponse {
|
||||
response := common.AgentResponse{Id: requestID}
|
||||
switch v := data.(type) {
|
||||
case *system.CombinedData:
|
||||
response.SystemData = v
|
||||
case *common.FingerprintResponse:
|
||||
response.Fingerprint = v
|
||||
case string:
|
||||
response.String = &v
|
||||
case map[string]smart.SmartData:
|
||||
response.SmartData = v
|
||||
case systemd.ServiceDetails:
|
||||
response.ServiceInfo = v
|
||||
default:
|
||||
// For unknown types, use the generic Data field
|
||||
response.Data, _ = cbor.Marshal(data)
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -13,9 +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/henrygd/beszel/internal/entities/systemd"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
@@ -165,20 +163,9 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes
|
||||
}
|
||||
|
||||
// responder that writes AgentResponse to stdout
|
||||
// Uses legacy typed fields for backward compatibility with <= 0.17
|
||||
sshResponder := func(data any, requestID *uint32) error {
|
||||
response := common.AgentResponse{Id: requestID}
|
||||
switch v := data.(type) {
|
||||
case *system.CombinedData:
|
||||
response.SystemData = v
|
||||
case string:
|
||||
response.String = &v
|
||||
case map[string]smart.SmartData:
|
||||
response.SmartData = v
|
||||
case systemd.ServiceDetails:
|
||||
response.ServiceInfo = v
|
||||
default:
|
||||
response.Error = fmt.Sprintf("unsupported response type: %T", data)
|
||||
}
|
||||
response := newAgentResponse(data, requestID)
|
||||
return cbor.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
|
||||
184
agent/smart.go
184
agent/smart.go
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// SmartManager manages data collection for SMART devices
|
||||
@@ -54,6 +54,12 @@ type DeviceInfo struct {
|
||||
parserType string
|
||||
}
|
||||
|
||||
// deviceKey is a composite key for a device, used to identify a device uniquely.
|
||||
type deviceKey struct {
|
||||
name string
|
||||
deviceType string
|
||||
}
|
||||
|
||||
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
||||
|
||||
// Refresh updates SMART data for all known devices
|
||||
@@ -165,7 +171,7 @@ func (sm *SmartManager) ScanDevices(force bool) error {
|
||||
configuredDevices = parsedDevices
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, sm.binPath, "--scan", "-j")
|
||||
@@ -202,7 +208,11 @@ func (sm *SmartManager) ScanDevices(force bool) error {
|
||||
}
|
||||
|
||||
func (sm *SmartManager) parseConfiguredDevices(config string) ([]*DeviceInfo, error) {
|
||||
entries := strings.Split(config, ",")
|
||||
splitChar := os.Getenv("SMART_DEVICES_SEPARATOR")
|
||||
if splitChar == "" {
|
||||
splitChar = ","
|
||||
}
|
||||
entries := strings.Split(config, splitChar)
|
||||
devices := make([]*DeviceInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
@@ -326,6 +336,13 @@ func normalizeParserType(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// makeDeviceKey creates a composite key from device name and type.
|
||||
// This allows multiple drives under the same device path (e.g., RAID controllers)
|
||||
// to be tracked separately.
|
||||
func makeDeviceKey(name, deviceType string) deviceKey {
|
||||
return deviceKey{name: name, deviceType: deviceType}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -435,7 +452,7 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
||||
defer cancel()
|
||||
|
||||
// Try with -n standby first if we have existing data
|
||||
args := sm.smartctlArgs(deviceInfo, true)
|
||||
args := sm.smartctlArgs(deviceInfo, hasExistingData)
|
||||
cmd := exec.CommandContext(ctx, sm.binPath, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
@@ -498,10 +515,12 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
||||
// 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)
|
||||
args := make([]string, 0, 9)
|
||||
var deviceType, parserType string
|
||||
|
||||
if deviceInfo != nil {
|
||||
deviceType := strings.ToLower(deviceInfo.Type)
|
||||
deviceType = strings.ToLower(deviceInfo.Type)
|
||||
parserType = strings.ToLower(deviceInfo.parserType)
|
||||
// types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345
|
||||
if deviceType != "" && deviceType != "scsi" && deviceType != "ata" {
|
||||
args = append(args, "-d", deviceInfo.Type)
|
||||
@@ -509,6 +528,13 @@ func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool
|
||||
}
|
||||
|
||||
args = append(args, "-a", "--json=c")
|
||||
effectiveType := parserType
|
||||
if effectiveType == "" {
|
||||
effectiveType = deviceType
|
||||
}
|
||||
if effectiveType == "sat" || effectiveType == "ata" {
|
||||
args = append(args, "-l", "devstat")
|
||||
}
|
||||
|
||||
if includeStandby {
|
||||
args = append(args, "-n", "standby")
|
||||
@@ -569,6 +595,28 @@ func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo
|
||||
return existing
|
||||
}
|
||||
|
||||
// buildUniqueNameIndex returns devices that appear exactly once by name.
|
||||
// It is used to safely apply name-only fallbacks without RAID ambiguity.
|
||||
buildUniqueNameIndex := func(devices []*DeviceInfo) map[string]*DeviceInfo {
|
||||
counts := make(map[string]int, len(devices))
|
||||
for _, dev := range devices {
|
||||
if dev == nil || dev.Name == "" {
|
||||
continue
|
||||
}
|
||||
counts[dev.Name]++
|
||||
}
|
||||
unique := make(map[string]*DeviceInfo, len(counts))
|
||||
for _, dev := range devices {
|
||||
if dev == nil || dev.Name == "" {
|
||||
continue
|
||||
}
|
||||
if counts[dev.Name] == 1 {
|
||||
unique[dev.Name] = dev
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// preserveVerifiedType copies the verified type/parser metadata from an existing
|
||||
// device record so that subsequent scans/config updates never downgrade a
|
||||
// previously verified device.
|
||||
@@ -581,69 +629,90 @@ func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo
|
||||
target.parserType = prev.parserType
|
||||
}
|
||||
|
||||
existingIndex := make(map[string]*DeviceInfo, len(existing))
|
||||
// applyConfiguredMetadata updates a matched device with any configured
|
||||
// overrides, preserving verified type data when present.
|
||||
applyConfiguredMetadata := func(existingDev, configuredDev *DeviceInfo) {
|
||||
// Only update the type if it has not been verified yet; otherwise we
|
||||
// keep the existing verified metadata intact.
|
||||
if configuredDev.Type != "" && !existingDev.typeVerified {
|
||||
newType := strings.TrimSpace(configuredDev.Type)
|
||||
existingDev.Type = newType
|
||||
existingDev.typeVerified = false
|
||||
existingDev.parserType = normalizeParserType(newType)
|
||||
}
|
||||
if configuredDev.InfoName != "" {
|
||||
existingDev.InfoName = configuredDev.InfoName
|
||||
}
|
||||
if configuredDev.Protocol != "" {
|
||||
existingDev.Protocol = configuredDev.Protocol
|
||||
}
|
||||
}
|
||||
|
||||
existingIndex := make(map[deviceKey]*DeviceInfo, len(existing))
|
||||
for _, dev := range existing {
|
||||
if dev == nil || dev.Name == "" {
|
||||
continue
|
||||
}
|
||||
existingIndex[dev.Name] = dev
|
||||
existingIndex[makeDeviceKey(dev.Name, dev.Type)] = dev
|
||||
}
|
||||
existingByName := buildUniqueNameIndex(existing)
|
||||
|
||||
finalDevices := make([]*DeviceInfo, 0, len(scanned)+len(configured))
|
||||
deviceIndex := make(map[string]*DeviceInfo, len(scanned)+len(configured))
|
||||
deviceIndex := make(map[deviceKey]*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 == "" {
|
||||
for _, scannedDevice := range scanned {
|
||||
if scannedDevice == nil || scannedDevice.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 {
|
||||
copyDev := *scannedDevice
|
||||
key := makeDeviceKey(copyDev.Name, copyDev.Type)
|
||||
if prev := existingIndex[key]; prev != nil {
|
||||
preserveVerifiedType(©Dev, prev)
|
||||
} else if prev := existingByName[copyDev.Name]; prev != nil {
|
||||
preserveVerifiedType(©Dev, prev)
|
||||
}
|
||||
|
||||
finalDevices = append(finalDevices, ©Dev)
|
||||
deviceIndex[copyDev.Name] = finalDevices[len(finalDevices)-1]
|
||||
copyKey := makeDeviceKey(copyDev.Name, copyDev.Type)
|
||||
deviceIndex[copyKey] = finalDevices[len(finalDevices)-1]
|
||||
}
|
||||
deviceIndexByName := buildUniqueNameIndex(finalDevices)
|
||||
|
||||
// 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 == "" {
|
||||
for _, configuredDevice := range configured {
|
||||
if configuredDevice == nil || configuredDevice.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
|
||||
}
|
||||
key := makeDeviceKey(configuredDevice.Name, configuredDevice.Type)
|
||||
if existingDev, ok := deviceIndex[key]; ok {
|
||||
applyConfiguredMetadata(existingDev, configuredDevice)
|
||||
continue
|
||||
}
|
||||
if existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {
|
||||
applyConfiguredMetadata(existingDev, configuredDevice)
|
||||
continue
|
||||
}
|
||||
|
||||
copyDev := *dev
|
||||
if prev := existingIndex[copyDev.Name]; prev != nil {
|
||||
copyDev := *configuredDevice
|
||||
key = makeDeviceKey(copyDev.Name, copyDev.Type)
|
||||
if prev := existingIndex[key]; prev != nil {
|
||||
preserveVerifiedType(©Dev, prev)
|
||||
} else if prev := existingByName[copyDev.Name]; prev != nil {
|
||||
preserveVerifiedType(©Dev, prev)
|
||||
} else if copyDev.Type != "" {
|
||||
copyDev.parserType = normalizeParserType(copyDev.Type)
|
||||
}
|
||||
|
||||
finalDevices = append(finalDevices, ©Dev)
|
||||
deviceIndex[copyDev.Name] = finalDevices[len(finalDevices)-1]
|
||||
copyKey := makeDeviceKey(copyDev.Name, copyDev.Type)
|
||||
deviceIndex[copyKey] = finalDevices[len(finalDevices)-1]
|
||||
}
|
||||
|
||||
return finalDevices
|
||||
@@ -661,12 +730,14 @@ func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
validNames := make(map[string]struct{}, len(devices))
|
||||
validKeys := make(map[deviceKey]struct{}, len(devices))
|
||||
nameCounts := make(map[string]int, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil || device.Name == "" {
|
||||
continue
|
||||
}
|
||||
validNames[device.Name] = struct{}{}
|
||||
validKeys[makeDeviceKey(device.Name, device.Type)] = struct{}{}
|
||||
nameCounts[device.Name]++
|
||||
}
|
||||
|
||||
for key, data := range sm.SmartDataMap {
|
||||
@@ -675,7 +746,11 @@ func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validNames[data.DiskName]; ok {
|
||||
if data.DiskType == "" {
|
||||
if nameCounts[data.DiskName] == 1 {
|
||||
continue
|
||||
}
|
||||
} else if _, ok := validKeys[makeDeviceKey(data.DiskName, data.DiskType)]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -763,6 +838,11 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
||||
smartData.FirmwareVersion = data.FirmwareVersion
|
||||
smartData.Capacity = data.UserCapacity.Bytes
|
||||
smartData.Temperature = data.Temperature.Current
|
||||
if smartData.Temperature == 0 {
|
||||
if temp, ok := temperatureFromAtaDeviceStatistics(data.AtaDeviceStatistics); ok {
|
||||
smartData.Temperature = temp
|
||||
}
|
||||
}
|
||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||
smartData.DiskName = data.Device.Name
|
||||
smartData.DiskType = data.Device.Type
|
||||
@@ -801,6 +881,36 @@ func getSmartStatus(temperature uint8, passed bool) string {
|
||||
}
|
||||
}
|
||||
|
||||
func temperatureFromAtaDeviceStatistics(stats smart.AtaDeviceStatistics) (uint8, bool) {
|
||||
entry := findAtaDeviceStatisticsEntry(stats, 5, "Current Temperature")
|
||||
if entry == nil || entry.Value == nil {
|
||||
return 0, false
|
||||
}
|
||||
if *entry.Value > 255 {
|
||||
return 0, false
|
||||
}
|
||||
return uint8(*entry.Value), true
|
||||
}
|
||||
|
||||
// findAtaDeviceStatisticsEntry centralizes ATA devstat lookups so additional
|
||||
// metrics can be pulled from the same structure in the future.
|
||||
func findAtaDeviceStatisticsEntry(stats smart.AtaDeviceStatistics, pageNumber uint8, entryName string) *smart.AtaDeviceStatisticsEntry {
|
||||
for pageIdx := range stats.Pages {
|
||||
page := &stats.Pages[pageIdx]
|
||||
if page.Number != pageNumber {
|
||||
continue
|
||||
}
|
||||
for entryIdx := range page.Table {
|
||||
entry := &page.Table[entryIdx]
|
||||
if !strings.EqualFold(entry.Name, entryName) {
|
||||
continue
|
||||
}
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
||||
var data smart.SmartInfoForScsi
|
||||
|
||||
|
||||
@@ -89,6 +89,39 @@ func TestParseSmartForSata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
||||
jsonPayload := []byte(`{
|
||||
"smartctl": {"exit_status": 0},
|
||||
"device": {"name": "/dev/sdb", "type": "sat"},
|
||||
"model_name": "SanDisk SSD U110 16GB",
|
||||
"serial_number": "DEVSTAT123",
|
||||
"firmware_version": "U21B001",
|
||||
"user_capacity": {"bytes": 16013942784},
|
||||
"smart_status": {"passed": true},
|
||||
"ata_smart_attributes": {"table": []},
|
||||
"ata_device_statistics": {
|
||||
"pages": [
|
||||
{
|
||||
"number": 5,
|
||||
"name": "Temperature Statistics",
|
||||
"table": [
|
||||
{"name": "Current Temperature", "value": 22, "flags": {"valid": true}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
||||
require.True(t, hasData)
|
||||
assert.Equal(t, 0, exitStatus)
|
||||
|
||||
deviceData, ok := sm.SmartDataMap["DEVSTAT123"]
|
||||
require.True(t, ok, "expected smart data entry for serial DEVSTAT123")
|
||||
assert.Equal(t, uint8(22), deviceData.Temperature)
|
||||
}
|
||||
|
||||
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
||||
jsonPayload := []byte(`{
|
||||
"smartctl": {"exit_status": 0},
|
||||
@@ -195,6 +228,24 @@ func TestDevicesSnapshotReturnsCopy(t *testing.T) {
|
||||
assert.Len(t, snapshot, 2)
|
||||
}
|
||||
|
||||
func TestScanDevicesWithEnvOverrideAndSeparator(t *testing.T) {
|
||||
t.Setenv("SMART_DEVICES_SEPARATOR", "|")
|
||||
t.Setenv("SMART_DEVICES", "/dev/sda:jmb39x-q,0|/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, "jmb39x-q,0", sm.SmartDevices[0].Type)
|
||||
assert.Equal(t, "/dev/nvme0", sm.SmartDevices[1].Name)
|
||||
assert.Equal(t, "nvme", sm.SmartDevices[1].Type)
|
||||
}
|
||||
|
||||
func TestScanDevicesWithEnvOverride(t *testing.T) {
|
||||
t.Setenv("SMART_DEVICES", "/dev/sda:sat, /dev/nvme0:nvme")
|
||||
|
||||
@@ -249,15 +300,21 @@ func TestSmartctlArgs(t *testing.T) {
|
||||
|
||||
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
|
||||
assert.Equal(t,
|
||||
[]string{"-d", "sat", "-a", "--json=c", "-n", "standby", "/dev/sda"},
|
||||
[]string{"-d", "sat", "-a", "--json=c", "-l", "devstat", "-n", "standby", "/dev/sda"},
|
||||
sm.smartctlArgs(sataDevice, true),
|
||||
)
|
||||
|
||||
assert.Equal(t,
|
||||
[]string{"-d", "sat", "-a", "--json=c", "/dev/sda"},
|
||||
[]string{"-d", "sat", "-a", "--json=c", "-l", "devstat", "/dev/sda"},
|
||||
sm.smartctlArgs(sataDevice, false),
|
||||
)
|
||||
|
||||
nvmeDevice := &DeviceInfo{Name: "/dev/nvme0", Type: "nvme"}
|
||||
assert.Equal(t,
|
||||
[]string{"-d", "nvme", "-a", "--json=c", "-n", "standby", "/dev/nvme0"},
|
||||
sm.smartctlArgs(nvmeDevice, true),
|
||||
)
|
||||
|
||||
assert.Equal(t,
|
||||
[]string{"-a", "--json=c", "-n", "standby"},
|
||||
sm.smartctlArgs(nil, true),
|
||||
@@ -442,6 +499,88 @@ func TestMergeDeviceListsUpdatesTypeWhenUnverified(t *testing.T) {
|
||||
assert.Equal(t, "", device.parserType)
|
||||
}
|
||||
|
||||
func TestMergeDeviceListsHandlesDevicesWithSameNameAndDifferentTypes(t *testing.T) {
|
||||
// There are use cases where the same device name is re-used,
|
||||
// for example, a RAID controller with multiple drives.
|
||||
scanned := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "megaraid,0"},
|
||||
{Name: "/dev/sda", Type: "megaraid,1"},
|
||||
{Name: "/dev/sda", Type: "megaraid,2"},
|
||||
}
|
||||
|
||||
merged := mergeDeviceLists(nil, scanned, nil)
|
||||
require.Len(t, merged, 3, "should have 3 separate devices for RAID controller")
|
||||
|
||||
byKey := make(map[string]*DeviceInfo, len(merged))
|
||||
for _, dev := range merged {
|
||||
key := dev.Name + "|" + dev.Type
|
||||
byKey[key] = dev
|
||||
}
|
||||
|
||||
assert.Contains(t, byKey, "/dev/sda|megaraid,0")
|
||||
assert.Contains(t, byKey, "/dev/sda|megaraid,1")
|
||||
assert.Contains(t, byKey, "/dev/sda|megaraid,2")
|
||||
}
|
||||
|
||||
func TestMergeDeviceListsHandlesMixedRAIDAndRegular(t *testing.T) {
|
||||
// Test mixing RAID drives with regular devices
|
||||
scanned := []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "megaraid,0"},
|
||||
{Name: "/dev/sda", Type: "megaraid,1"},
|
||||
{Name: "/dev/sdb", Type: "sat"},
|
||||
{Name: "/dev/nvme0", Type: "nvme"},
|
||||
}
|
||||
|
||||
merged := mergeDeviceLists(nil, scanned, nil)
|
||||
require.Len(t, merged, 4, "should have 4 separate devices")
|
||||
|
||||
byKey := make(map[string]*DeviceInfo, len(merged))
|
||||
for _, dev := range merged {
|
||||
key := dev.Name + "|" + dev.Type
|
||||
byKey[key] = dev
|
||||
}
|
||||
|
||||
assert.Contains(t, byKey, "/dev/sda|megaraid,0")
|
||||
assert.Contains(t, byKey, "/dev/sda|megaraid,1")
|
||||
assert.Contains(t, byKey, "/dev/sdb|sat")
|
||||
assert.Contains(t, byKey, "/dev/nvme0|nvme")
|
||||
}
|
||||
|
||||
func TestUpdateSmartDevicesPreservesRAIDDrives(t *testing.T) {
|
||||
// Test that updateSmartDevices correctly validates RAID drives using composite keys
|
||||
sm := &SmartManager{
|
||||
SmartDevices: []*DeviceInfo{
|
||||
{Name: "/dev/sda", Type: "megaraid,0"},
|
||||
{Name: "/dev/sda", Type: "megaraid,1"},
|
||||
},
|
||||
SmartDataMap: map[string]*smart.SmartData{
|
||||
"serial-0": {
|
||||
DiskName: "/dev/sda",
|
||||
DiskType: "megaraid,0",
|
||||
SerialNumber: "serial-0",
|
||||
},
|
||||
"serial-1": {
|
||||
DiskName: "/dev/sda",
|
||||
DiskType: "megaraid,1",
|
||||
SerialNumber: "serial-1",
|
||||
},
|
||||
"serial-stale": {
|
||||
DiskName: "/dev/sda",
|
||||
DiskType: "megaraid,2",
|
||||
SerialNumber: "serial-stale",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sm.updateSmartDevices(sm.SmartDevices)
|
||||
|
||||
// serial-0 and serial-1 should be preserved (matching devices exist)
|
||||
assert.Contains(t, sm.SmartDataMap, "serial-0")
|
||||
assert.Contains(t, sm.SmartDataMap, "serial-1")
|
||||
// serial-stale should be removed (no matching device)
|
||||
assert.NotContains(t, sm.SmartDataMap, "serial-stale")
|
||||
}
|
||||
|
||||
func TestParseSmartOutputMarksVerified(t *testing.T) {
|
||||
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
||||
data, err := os.ReadFile(fixturePath)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"log/slog"
|
||||
"maps"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -28,11 +29,36 @@ type systemdManager struct {
|
||||
patterns []string
|
||||
}
|
||||
|
||||
// isSystemdAvailable checks if systemd is used on the system to avoid unnecessary connection attempts (#1548)
|
||||
func isSystemdAvailable() bool {
|
||||
paths := []string{
|
||||
"/run/systemd/system",
|
||||
"/run/dbus/system_bus_socket",
|
||||
"/var/run/dbus/system_bus_socket",
|
||||
}
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if data, err := os.ReadFile("/proc/1/comm"); err == nil {
|
||||
return strings.TrimSpace(string(data)) == "systemd"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newSystemdManager creates a new systemdManager.
|
||||
func newSystemdManager() (*systemdManager, error) {
|
||||
if skipSystemd, _ := GetEnv("SKIP_SYSTEMD"); skipSystemd == "true" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if systemd is available on the system before attempting connection
|
||||
if !isSystemdAvailable() {
|
||||
slog.Debug("Systemd not available")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
conn, err := dbus.NewSystemConnectionContext(context.Background())
|
||||
if err != nil {
|
||||
slog.Debug("Error connecting to systemd", "err", err, "ref", "https://beszel.dev/guide/systemd")
|
||||
@@ -118,13 +144,27 @@ func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*syst
|
||||
return nil
|
||||
}
|
||||
|
||||
// Track which units are currently present to remove stale entries
|
||||
currentUnits := make(map[string]struct{}, len(units))
|
||||
|
||||
for _, unit := range units {
|
||||
currentUnits[unit.Name] = struct{}{}
|
||||
service, err := sm.updateServiceStats(conn, unit)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
// Remove services that no longer exist in systemd
|
||||
sm.Lock()
|
||||
for unitName := range sm.serviceStatsMap {
|
||||
if _, exists := currentUnits[unitName]; !exists {
|
||||
delete(sm.serviceStatsMap, unitName)
|
||||
}
|
||||
}
|
||||
sm.Unlock()
|
||||
|
||||
sm.hasFreshStats = true
|
||||
return services
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func TestSystemdManagerGetServiceStats(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with refresh = true
|
||||
result := manager.getServiceStats(true)
|
||||
result := manager.getServiceStats("any-service", true)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// Test with refresh = false
|
||||
result = manager.getServiceStats(false)
|
||||
result = manager.getServiceStats("any-service", false)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -48,6 +49,35 @@ func TestUnescapeServiceNameInvalid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSystemdAvailable(t *testing.T) {
|
||||
// Note: This test's result will vary based on the actual system running the tests
|
||||
// On systems with systemd, it should return true
|
||||
// On systems without systemd, it should return false
|
||||
result := isSystemdAvailable()
|
||||
|
||||
// Check if either the /run/systemd/system directory exists or PID 1 is systemd
|
||||
runSystemdExists := false
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
runSystemdExists = true
|
||||
}
|
||||
|
||||
pid1IsSystemd := false
|
||||
if data, err := os.ReadFile("/proc/1/comm"); err == nil {
|
||||
pid1IsSystemd = strings.TrimSpace(string(data)) == "systemd"
|
||||
}
|
||||
|
||||
expected := runSystemdExists || pid1IsSystemd
|
||||
|
||||
assert.Equal(t, expected, result, "isSystemdAvailable should correctly detect systemd presence")
|
||||
|
||||
// Log the result for informational purposes
|
||||
if result {
|
||||
t.Log("Systemd is available on this system")
|
||||
} else {
|
||||
t.Log("Systemd is not available on this system")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetServicePatterns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel/internal/ghupdate"
|
||||
)
|
||||
@@ -108,12 +106,12 @@ func Update(useMirror bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 6) Fix SELinux context if necessary
|
||||
if err := handleSELinuxContext(exePath); err != nil {
|
||||
// Fix SELinux context if necessary
|
||||
if err := ghupdate.HandleSELinuxContext(exePath); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: SELinux context handling: %v", err)
|
||||
}
|
||||
|
||||
// 7) Restart service if running under a recognised init system
|
||||
// Restart service if running under a recognised init system
|
||||
if r := detectRestarter(); r != nil {
|
||||
if err := r.Restart(); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to restart service: %v", err)
|
||||
@@ -128,41 +126,3 @@ func Update(useMirror bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSELinuxContext restores or applies the correct SELinux label to the binary.
|
||||
func handleSELinuxContext(path string) error {
|
||||
out, err := exec.Command("getenforce").Output()
|
||||
if err != nil {
|
||||
// SELinux not enabled or getenforce not available
|
||||
return nil
|
||||
}
|
||||
state := strings.TrimSpace(string(out))
|
||||
if state == "Disabled" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "SELinux is enabled; applying context…")
|
||||
var errs []string
|
||||
|
||||
// Try persistent context via semanage+restorecon
|
||||
if semanagePath, err := exec.LookPath("semanage"); err == nil {
|
||||
if err := exec.Command(semanagePath, "fcontext", "-a", "-t", "bin_t", path).Run(); err != nil {
|
||||
errs = append(errs, "semanage fcontext failed: "+err.Error())
|
||||
} else if restoreconPath, err := exec.LookPath("restorecon"); err == nil {
|
||||
if err := exec.Command(restoreconPath, "-v", path).Run(); err != nil {
|
||||
errs = append(errs, "restorecon failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to temporary context via chcon
|
||||
if chconPath, err := exec.LookPath("chcon"); err == nil {
|
||||
if err := exec.Command(chconPath, "-t", "bin_t", path).Run(); err != nil {
|
||||
errs = append(errs, "chcon failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("SELinux context errors: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
// Version is the current version of the application.
|
||||
Version = "0.18.0-beta.1"
|
||||
Version = "0.18.2"
|
||||
// AppName is the name of the application.
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
34
go.mod
34
go.mod
@@ -11,17 +11,17 @@ 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.12.1
|
||||
github.com/nicholas-fedor/shoutrrr v0.13.1
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.34.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.10
|
||||
github.com/pocketbase/pocketbase v0.35.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||
golang.org/x/sys v0.40.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -34,15 +34,15 @@ require (
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // 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/godbus/dbus/v5 v5.2.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // 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.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // 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
|
||||
@@ -54,15 +54,15 @@ require (
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/image v0.33.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
howett.net/plist v1.0.1 // 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.40.1 // indirect
|
||||
modernc.org/sqlite v1.43.0 // indirect
|
||||
)
|
||||
|
||||
98
go.sum
98
go.sum
@@ -33,8 +33,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.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/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=
|
||||
@@ -51,8 +51,8 @@ github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtS
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -62,13 +62,15 @@ github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLx
|
||||
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jarcoal/httpmock v1.4.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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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=
|
||||
@@ -83,19 +85,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8=
|
||||
github.com/nicholas-fedor/shoutrrr v0.12.1/go.mod h1:64qWuPpvTUv9ZppEoR6OdroiFmgf9w11YSaR0h9KZGg=
|
||||
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/nicholas-fedor/shoutrrr v0.13.1 h1:llEoHNbnMM4GfQ9+2Ns3n6ssvNfi3NPWluM0AQiicoY=
|
||||
github.com/nicholas-fedor/shoutrrr v0.13.1/go.mod h1:kU4cFJpEAtTzl3iV0l+XUXmM90OlC5T01b7roM4/pYM=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.34.0 h1:5W80PrGvkRYIMAIK90F7w031/hXgZVz1KSuCJqSpgJo=
|
||||
github.com/pocketbase/pocketbase v0.34.0/go.mod h1:K/9z/Zb9PR9yW2Qyoc73jHV/EKT8cMTk9bQWyrzYlvI=
|
||||
github.com/pocketbase/pocketbase v0.35.1 h1:Cd5ivUThTw29myY/tYa2cb0elkScBMseG6fExZsIQB8=
|
||||
github.com/pocketbase/pocketbase v0.35.1/go.mod h1:yQnh1o1Aq6wVuqcmZbRbDmIhc31AME/F5pnPR0Bdtmg=
|
||||
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=
|
||||
@@ -103,12 +105,12 @@ 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.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
|
||||
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
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=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -127,38 +129,38 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
@@ -171,18 +173,22 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
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.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
||||
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -191,8 +197,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.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
|
||||
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
@@ -34,14 +35,14 @@ 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"`
|
||||
SystemData *system.CombinedData `cbor:"1,keyasint,omitempty,omitzero"` // Legacy (<= 0.17)
|
||||
Fingerprint *FingerprintResponse `cbor:"2,keyasint,omitempty,omitzero"` // Legacy (<= 0.17)
|
||||
Error string `cbor:"3,keyasint,omitempty,omitzero"`
|
||||
String *string `cbor:"4,keyasint,omitempty,omitzero"`
|
||||
SmartData map[string]smart.SmartData `cbor:"5,keyasint,omitempty,omitzero"`
|
||||
ServiceInfo systemd.ServiceDetails `cbor:"6,keyasint,omitempty,omitzero"`
|
||||
// Logs *LogsPayload `cbor:"4,keyasint,omitempty,omitzero"`
|
||||
// RawBytes []byte `cbor:"4,keyasint,omitempty,omitzero"`
|
||||
String *string `cbor:"4,keyasint,omitempty,omitzero"` // Legacy (<= 0.17)
|
||||
SmartData map[string]smart.SmartData `cbor:"5,keyasint,omitempty,omitzero"` // Legacy (<= 0.17)
|
||||
ServiceInfo systemd.ServiceDetails `cbor:"6,keyasint,omitempty,omitzero"` // Legacy (<= 0.17)
|
||||
// Data is the generic response payload for new endpoints (0.18+)
|
||||
Data cbor.RawMessage `cbor:"7,keyasint,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
type FingerprintRequest struct {
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN rm -rf /tmp/*
|
||||
# --------------------------
|
||||
# Final image: default scratch-based agent
|
||||
# --------------------------
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
RUN apk add --no-cache smartmontools
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
|
||||
# Final image
|
||||
# Note: must cap_add: [CAP_PERFMON] and mount /dev/dri/ as volume
|
||||
# --------------------------
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,7 +10,7 @@ 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 CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags glibc -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
# --------------------------
|
||||
# Smartmontools builder stage
|
||||
|
||||
@@ -130,10 +130,23 @@ type SummaryInfo struct {
|
||||
}
|
||||
|
||||
type AtaSmartAttributes struct {
|
||||
// Revision int `json:"revision"`
|
||||
Table []AtaSmartAttribute `json:"table"`
|
||||
}
|
||||
|
||||
type AtaDeviceStatistics struct {
|
||||
Pages []AtaDeviceStatisticsPage `json:"pages"`
|
||||
}
|
||||
|
||||
type AtaDeviceStatisticsPage struct {
|
||||
Number uint8 `json:"number"`
|
||||
Table []AtaDeviceStatisticsEntry `json:"table"`
|
||||
}
|
||||
|
||||
type AtaDeviceStatisticsEntry struct {
|
||||
Name string `json:"name"`
|
||||
Value *uint64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type AtaSmartAttribute struct {
|
||||
ID uint16 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -343,7 +356,8 @@ type SmartInfoForSata struct {
|
||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
||||
// AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"`
|
||||
AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"`
|
||||
AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"`
|
||||
AtaDeviceStatistics AtaDeviceStatistics `json:"ata_device_statistics"`
|
||||
// PowerOnTime PowerOnTimeInfo `json:"power_on_time"`
|
||||
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||
Temperature TemperatureInfo `json:"temperature"`
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -345,5 +346,32 @@ func archiveSuffix(binaryName, goos, goarch string) string {
|
||||
if goos == "windows" {
|
||||
return fmt.Sprintf("%s_%s_%s.zip", binaryName, goos, goarch)
|
||||
}
|
||||
// Use glibc build for agent on glibc systems (includes NVML support via purego)
|
||||
if binaryName == "beszel-agent" && goos == "linux" && goarch == "amd64" && isGlibc() {
|
||||
return fmt.Sprintf("%s_%s_%s_glibc.tar.gz", binaryName, goos, goarch)
|
||||
}
|
||||
return fmt.Sprintf("%s_%s_%s.tar.gz", binaryName, goos, goarch)
|
||||
}
|
||||
|
||||
func isGlibc() bool {
|
||||
for _, path := range []string{
|
||||
"/lib64/ld-linux-x86-64.so.2", // common on many distros
|
||||
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", // Debian/Ubuntu
|
||||
"/lib/ld-linux-x86-64.so.2", // alternate
|
||||
} {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Fallback to ldd output when present (musl ldd reports musl, glibc reports GNU libc/glibc).
|
||||
if lddPath, err := exec.LookPath("ldd"); err == nil {
|
||||
out, err := exec.Command(lddPath, "--version").CombinedOutput()
|
||||
if err == nil {
|
||||
s := strings.ToLower(string(out))
|
||||
if strings.Contains(s, "gnu libc") || strings.Contains(s, "glibc") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
66
internal/ghupdate/selinux.go
Normal file
66
internal/ghupdate/selinux.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package ghupdate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HandleSELinuxContext restores or applies the correct SELinux label to the binary.
|
||||
func HandleSELinuxContext(path string) error {
|
||||
out, err := exec.Command("getenforce").Output()
|
||||
if err != nil {
|
||||
// SELinux not enabled or getenforce not available
|
||||
return nil
|
||||
}
|
||||
state := strings.TrimSpace(string(out))
|
||||
if state == "Disabled" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ColorPrint(ColorYellow, "SELinux is enabled; applying context…")
|
||||
|
||||
// Try persistent context via semanage+restorecon
|
||||
if success := trySemanageRestorecon(path); success {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback to temporary context via chcon
|
||||
if chconPath, err := exec.LookPath("chcon"); err == nil {
|
||||
if err := exec.Command(chconPath, "-t", "bin_t", path).Run(); err != nil {
|
||||
return fmt.Errorf("chcon failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("no SELinux tools available (semanage/restorecon or chcon)")
|
||||
}
|
||||
|
||||
// trySemanageRestorecon attempts to set persistent SELinux context using semanage and restorecon.
|
||||
// Returns true if successful, false otherwise.
|
||||
func trySemanageRestorecon(path string) bool {
|
||||
semanagePath, err := exec.LookPath("semanage")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
restoreconPath, err := exec.LookPath("restorecon")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to add the fcontext rule; if it already exists, try to modify it
|
||||
if err := exec.Command(semanagePath, "fcontext", "-a", "-t", "bin_t", path).Run(); err != nil {
|
||||
// Rule may already exist, try modify instead
|
||||
if err := exec.Command(semanagePath, "fcontext", "-m", "-t", "bin_t", path).Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the context with restorecon
|
||||
if err := exec.Command(restoreconPath, "-v", path).Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
53
internal/ghupdate/selinux_test.go
Normal file
53
internal/ghupdate/selinux_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package ghupdate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleSELinuxContext_NoSELinux(t *testing.T) {
|
||||
// Skip on SELinux systems - this test is for non-SELinux behavior
|
||||
if _, err := exec.LookPath("getenforce"); err == nil {
|
||||
t.Skip("skipping on SELinux-enabled system")
|
||||
}
|
||||
|
||||
// On systems without SELinux, getenforce will fail and the function
|
||||
// should return nil without error
|
||||
tempFile := filepath.Join(t.TempDir(), "test-binary")
|
||||
if err := os.WriteFile(tempFile, []byte("test"), 0755); err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
err := HandleSELinuxContext(tempFile)
|
||||
if err != nil {
|
||||
t.Errorf("HandleSELinuxContext() on non-SELinux system returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSELinuxContext_InvalidPath(t *testing.T) {
|
||||
// Skip on SELinux systems - this test is for non-SELinux behavior
|
||||
if _, err := exec.LookPath("getenforce"); err == nil {
|
||||
t.Skip("skipping on SELinux-enabled system")
|
||||
}
|
||||
|
||||
// On non-SELinux systems, getenforce fails early so even invalid paths succeed
|
||||
err := HandleSELinuxContext("/nonexistent/path/binary")
|
||||
if err != nil {
|
||||
t.Errorf("HandleSELinuxContext() with invalid path on non-SELinux system returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrySemanageRestorecon_NoTools(t *testing.T) {
|
||||
// Skip if semanage is available (we don't want to modify system SELinux policy)
|
||||
if _, err := exec.LookPath("semanage"); err == nil {
|
||||
t.Skip("skipping on system with semanage available")
|
||||
}
|
||||
|
||||
// Should return false when semanage is not available
|
||||
result := trySemanageRestorecon("/some/path")
|
||||
if result {
|
||||
t.Error("trySemanageRestorecon() returned true when semanage is not available")
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,15 @@ func (acr *agentConnectRequest) agentConnect() (err error) {
|
||||
|
||||
// Check if token is an active universal token
|
||||
acr.userId, acr.isUniversalToken = universalTokenMap.GetMap().GetOk(acr.token)
|
||||
if !acr.isUniversalToken {
|
||||
// Fallback: check for a permanent universal token stored in the DB
|
||||
if rec, err := acr.hub.FindFirstRecordByFilter("universal_tokens", "token = {:token}", dbx.Params{"token": acr.token}); err == nil {
|
||||
if userID := rec.GetString("user"); userID != "" {
|
||||
acr.userId = userID
|
||||
acr.isUniversalToken = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find matching fingerprint records for this token
|
||||
fpRecords := getFingerprintRecordsByToken(acr.token, acr.hub)
|
||||
|
||||
@@ -1169,6 +1169,106 @@ func TestMultipleSystemsWithSameUniversalToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermanentUniversalTokenFromDB verifies that a universal token persisted in the DB
|
||||
// (universal_tokens collection) is accepted for agent self-registration even if it is not
|
||||
// present in the in-memory universalTokenMap.
|
||||
func TestPermanentUniversalTokenFromDB(t *testing.T) {
|
||||
// Create hub and test app
|
||||
hub, testApp, err := createTestHub(t)
|
||||
require.NoError(t, err)
|
||||
defer testApp.Cleanup()
|
||||
|
||||
// Get the hub's SSH key
|
||||
hubSigner, err := hub.GetSSHKey("")
|
||||
require.NoError(t, err)
|
||||
goodPubKey := hubSigner.PublicKey()
|
||||
|
||||
// Create test user
|
||||
userRecord, err := createTestUser(testApp)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a permanent universal token record in the DB (do NOT add it to universalTokenMap)
|
||||
universalToken := "db-universal-token-123"
|
||||
_, err = createTestRecord(testApp, "universal_tokens", map[string]any{
|
||||
"user": userRecord.Id,
|
||||
"token": universalToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create HTTP server with the actual API route
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/beszel/agent-connect" {
|
||||
acr := &agentConnectRequest{
|
||||
hub: hub,
|
||||
req: r,
|
||||
res: w,
|
||||
}
|
||||
acr.agentConnect()
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Create and configure agent
|
||||
agentDataDir := t.TempDir()
|
||||
err = os.WriteFile(filepath.Join(agentDataDir, "fingerprint"), []byte("db-token-system-fingerprint"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAgent, err := agent.NewAgent(agentDataDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set up environment variables for the agent
|
||||
os.Setenv("BESZEL_AGENT_HUB_URL", ts.URL)
|
||||
os.Setenv("BESZEL_AGENT_TOKEN", universalToken)
|
||||
defer func() {
|
||||
os.Unsetenv("BESZEL_AGENT_HUB_URL")
|
||||
os.Unsetenv("BESZEL_AGENT_TOKEN")
|
||||
}()
|
||||
|
||||
// Start agent in background
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
serverOptions := agent.ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: "127.0.0.1:46050",
|
||||
Keys: []ssh.PublicKey{goodPubKey},
|
||||
}
|
||||
done <- testAgent.Start(serverOptions)
|
||||
}()
|
||||
|
||||
// Wait for connection result
|
||||
maxWait := 2 * time.Second
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
checkInterval := 20 * time.Millisecond
|
||||
timeout := time.After(maxWait)
|
||||
ticker := time.Tick(checkInterval)
|
||||
|
||||
connectionManager := testAgent.GetConnectionManager()
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
t.Fatalf("Expected connection to succeed but timed out - agent state: %d", connectionManager.State)
|
||||
case <-ticker:
|
||||
if connectionManager.State == agent.WebSocketConnected {
|
||||
// Success
|
||||
goto verify
|
||||
}
|
||||
case err := <-done:
|
||||
// If Start returns early, treat it as failure
|
||||
if err != nil {
|
||||
t.Fatalf("Agent failed to start/connect: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verify:
|
||||
// Verify that a system was created for the user (self-registration path)
|
||||
systemsAfter, err := testApp.FindRecordsByFilter("systems", "users ~ {:userId}", "", -1, 0, map[string]any{"userId": userRecord.Id})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, systemsAfter, "Expected a system to be created for DB-backed universal token")
|
||||
}
|
||||
|
||||
// TestFindOrCreateSystemForToken tests the findOrCreateSystemForToken function
|
||||
func TestFindOrCreateSystemForToken(t *testing.T) {
|
||||
hub, testApp, err := createTestHub(t)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/henrygd/beszel/internal/users"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -193,7 +194,34 @@ func setCollectionAuthSettings(app core.App) error {
|
||||
}
|
||||
containersListRule := strings.Replace(systemsReadRule, "users.id", "system.users.id", 1)
|
||||
containersCollection.ListRule = &containersListRule
|
||||
return app.Save(containersCollection)
|
||||
if err := app.Save(containersCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// allow all users to access system-related collections if SHARE_ALL_SYSTEMS is set
|
||||
// these collections all have a "system" relation field
|
||||
systemRelatedCollections := []string{"system_details", "smart_devices", "systemd_services"}
|
||||
for _, collectionName := range systemRelatedCollections {
|
||||
collection, err := app.FindCollectionByNameOrId(collectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collection.ListRule = &containersListRule
|
||||
// set viewRule for collections that need it (system_details, smart_devices)
|
||||
if collection.ViewRule != nil {
|
||||
collection.ViewRule = &containersListRule
|
||||
}
|
||||
// set deleteRule for smart_devices (allows user to dismiss disk warnings)
|
||||
if collectionName == "smart_devices" {
|
||||
deleteRule := containersListRule + " && @request.auth.role != \"readonly\""
|
||||
collection.DeleteRule = &deleteRule
|
||||
}
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerCronJobs sets up scheduled tasks
|
||||
@@ -288,24 +316,90 @@ func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
|
||||
userID := e.Auth.Id
|
||||
query := e.Request.URL.Query()
|
||||
token := query.Get("token")
|
||||
enable := query.Get("enable")
|
||||
permanent := query.Get("permanent")
|
||||
|
||||
// helper for deleting any existing permanent token record for this user
|
||||
deletePermanent := func() error {
|
||||
rec, err := h.FindFirstRecordByFilter("universal_tokens", "user = {:user}", dbx.Params{"user": userID})
|
||||
if err != nil {
|
||||
return nil // no record
|
||||
}
|
||||
return h.Delete(rec)
|
||||
}
|
||||
|
||||
// helper for upserting a permanent token record for this user
|
||||
upsertPermanent := func(token string) error {
|
||||
rec, err := h.FindFirstRecordByFilter("universal_tokens", "user = {:user}", dbx.Params{"user": userID})
|
||||
if err == nil {
|
||||
rec.Set("token", token)
|
||||
return h.Save(rec)
|
||||
}
|
||||
|
||||
col, err := h.FindCachedCollectionByNameOrId("universal_tokens")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRec := core.NewRecord(col)
|
||||
newRec.Set("user", userID)
|
||||
newRec.Set("token", token)
|
||||
return h.Save(newRec)
|
||||
}
|
||||
|
||||
// Disable universal tokens (both ephemeral and permanent)
|
||||
if enable == "0" {
|
||||
tokenMap.RemovebyValue(userID)
|
||||
_ = deletePermanent()
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": false, "permanent": false})
|
||||
}
|
||||
|
||||
// Enable universal token (ephemeral or permanent)
|
||||
if enable == "1" {
|
||||
if token == "" {
|
||||
token = uuid.New().String()
|
||||
}
|
||||
|
||||
if permanent == "1" {
|
||||
// make token permanent (persist across restarts)
|
||||
tokenMap.RemovebyValue(userID)
|
||||
if err := upsertPermanent(token); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": true, "permanent": true})
|
||||
}
|
||||
|
||||
// default: ephemeral mode (1 hour)
|
||||
_ = deletePermanent()
|
||||
tokenMap.Set(token, userID, time.Hour)
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": true, "permanent": false})
|
||||
}
|
||||
|
||||
// Read current state
|
||||
// Prefer permanent token if it exists.
|
||||
if rec, err := h.FindFirstRecordByFilter("universal_tokens", "user = {:user}", dbx.Params{"user": userID}); err == nil {
|
||||
dbToken := rec.GetString("token")
|
||||
// If no token was provided, or the caller is asking about their permanent token, return it.
|
||||
if token == "" || token == dbToken {
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": dbToken, "active": true, "permanent": true})
|
||||
}
|
||||
// Token doesn't match their permanent token (avoid leaking other info)
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": false, "permanent": false})
|
||||
}
|
||||
|
||||
// No permanent token; fall back to ephemeral token map.
|
||||
if token == "" {
|
||||
// return existing token if it exists
|
||||
if token, _, ok := tokenMap.GetByValue(userID); ok {
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": true})
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": true, "permanent": false})
|
||||
}
|
||||
// if no token is provided, generate a new one
|
||||
token = uuid.New().String()
|
||||
}
|
||||
response := map[string]any{"token": token}
|
||||
|
||||
switch query.Get("enable") {
|
||||
case "1":
|
||||
tokenMap.Set(token, userID, time.Hour)
|
||||
case "0":
|
||||
tokenMap.RemovebyValue(userID)
|
||||
}
|
||||
_, response["active"] = tokenMap.GetOk(token)
|
||||
// Token is considered active only if it belongs to the current user.
|
||||
activeUser, ok := tokenMap.GetOk(token)
|
||||
active := ok && activeUser == userID
|
||||
response := map[string]any{"token": token, "active": active, "permanent": false}
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
@@ -378,7 +378,18 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"active", "token"},
|
||||
ExpectedContent: []string{"active", "token", "permanent"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
Name: "GET /universal-token - enable permanent should succeed",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/universal-token?enable=1&permanent=1&token=permanent-token-123",
|
||||
Headers: map[string]string{
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"\"permanent\":true", "permanent-token-123"},
|
||||
TestAppFactory: testAppFactory,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/hub/transport"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
|
||||
@@ -23,28 +25,30 @@ import (
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/lxzan/gws"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type System struct {
|
||||
Id string `db:"id"`
|
||||
Host string `db:"host"`
|
||||
Port string `db:"port"`
|
||||
Status string `db:"status"`
|
||||
manager *SystemManager // Manager that this system belongs to
|
||||
client *ssh.Client // SSH client for fetching data
|
||||
data *system.CombinedData // system data from agent
|
||||
ctx context.Context // Context for stopping the updater
|
||||
cancel context.CancelFunc // Stops and removes system from updater
|
||||
WsConn *ws.WsConn // Handler for agent WebSocket connection
|
||||
agentVersion semver.Version // Agent version
|
||||
updateTicker *time.Ticker // Ticker for updating the system
|
||||
detailsFetched atomic.Bool // True if static system details have been fetched and saved
|
||||
smartFetching atomic.Bool // True if SMART devices are currently being fetched
|
||||
smartInterval time.Duration // Interval for periodic SMART data updates
|
||||
lastSmartFetch atomic.Int64 // Unix milliseconds of last SMART data fetch
|
||||
Id string `db:"id"`
|
||||
Host string `db:"host"`
|
||||
Port string `db:"port"`
|
||||
Status string `db:"status"`
|
||||
manager *SystemManager // Manager that this system belongs to
|
||||
client *ssh.Client // SSH client for fetching data
|
||||
sshTransport *transport.SSHTransport // SSH transport for requests
|
||||
data *system.CombinedData // system data from agent
|
||||
ctx context.Context // Context for stopping the updater
|
||||
cancel context.CancelFunc // Stops and removes system from updater
|
||||
WsConn *ws.WsConn // Handler for agent WebSocket connection
|
||||
agentVersion semver.Version // Agent version
|
||||
updateTicker *time.Ticker // Ticker for updating the system
|
||||
detailsFetched atomic.Bool // True if static system details have been fetched and saved
|
||||
smartFetching atomic.Bool // True if SMART devices are currently being fetched
|
||||
smartInterval time.Duration // Interval for periodic SMART data updates
|
||||
lastSmartFetch atomic.Int64 // Unix milliseconds of last SMART data fetch
|
||||
}
|
||||
|
||||
func (sm *SystemManager) NewSystem(systemId string) *System {
|
||||
@@ -359,8 +363,78 @@ func (sys *System) getContext() (context.Context, context.CancelFunc) {
|
||||
return sys.ctx, sys.cancel
|
||||
}
|
||||
|
||||
// fetchDataFromAgent attempts to fetch data from the agent,
|
||||
// prioritizing WebSocket if available.
|
||||
// request sends a request to the agent, trying WebSocket first, then SSH.
|
||||
// This is the unified request method that uses the transport abstraction.
|
||||
func (sys *System) request(ctx context.Context, action common.WebSocketAction, req any, dest any) error {
|
||||
// Try WebSocket first
|
||||
if sys.WsConn != nil && sys.WsConn.IsConnected() {
|
||||
wsTransport := transport.NewWebSocketTransport(sys.WsConn)
|
||||
if err := wsTransport.Request(ctx, action, req, dest); err == nil {
|
||||
return nil
|
||||
} else if !shouldFallbackToSSH(err) {
|
||||
return err
|
||||
} else if shouldCloseWebSocket(err) {
|
||||
sys.closeWebSocketConnection()
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to SSH if WebSocket fails
|
||||
if err := sys.ensureSSHTransport(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := sys.sshTransport.RequestWithRetry(ctx, action, req, dest, 1)
|
||||
// Keep legacy SSH client/version fields in sync for other code paths.
|
||||
if sys.sshTransport != nil {
|
||||
sys.client = sys.sshTransport.GetClient()
|
||||
sys.agentVersion = sys.sshTransport.GetAgentVersion()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func shouldFallbackToSSH(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, gws.ErrConnClosed) {
|
||||
return true
|
||||
}
|
||||
return errors.Is(err, transport.ErrWebSocketNotConnected)
|
||||
}
|
||||
|
||||
func shouldCloseWebSocket(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return errors.Is(err, gws.ErrConnClosed) || errors.Is(err, transport.ErrWebSocketNotConnected)
|
||||
}
|
||||
|
||||
// ensureSSHTransport ensures the SSH transport is initialized and connected.
|
||||
func (sys *System) ensureSSHTransport() error {
|
||||
if sys.sshTransport == nil {
|
||||
if sys.manager.sshConfig == nil {
|
||||
if err := sys.manager.createSSHClientConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sys.sshTransport = transport.NewSSHTransport(transport.SSHTransportConfig{
|
||||
Host: sys.Host,
|
||||
Port: sys.Port,
|
||||
Config: sys.manager.sshConfig,
|
||||
Timeout: 4 * time.Second,
|
||||
})
|
||||
}
|
||||
// Sync client state with transport
|
||||
if sys.client != nil {
|
||||
sys.sshTransport.SetClient(sys.client)
|
||||
sys.sshTransport.SetAgentVersion(sys.agentVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchDataFromAgent attempts to fetch data from the agent, prioritizing WebSocket if available.
|
||||
func (sys *System) fetchDataFromAgent(options common.DataRequestOptions) (*system.CombinedData, error) {
|
||||
if sys.data == nil {
|
||||
sys.data = &system.CombinedData{}
|
||||
@@ -386,112 +460,47 @@ func (sys *System) fetchDataViaWebSocket(options common.DataRequestOptions) (*sy
|
||||
if sys.WsConn == nil || !sys.WsConn.IsConnected() {
|
||||
return nil, errors.New("no websocket connection")
|
||||
}
|
||||
err := sys.WsConn.RequestSystemData(context.Background(), sys.data, options)
|
||||
wsTransport := transport.NewWebSocketTransport(sys.WsConn)
|
||||
err := wsTransport.Request(context.Background(), common.GetData, options, sys.data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
var result string
|
||||
err := sys.request(ctx, common.GetContainerInfo, common.ContainerInfoRequest{ContainerID: containerID}, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 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")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
var result string
|
||||
err := sys.request(ctx, common.GetContainerLogs, common.ContainerLogsRequest{ContainerID: containerID}, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FetchSystemdInfoFromAgent fetches detailed systemd service information from the agent
|
||||
func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.ServiceDetails, 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.RequestSystemdInfo(ctx, serviceName)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
var result systemd.ServiceDetails
|
||||
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.GetSystemdInfo, Data: common.SystemdInfoRequest{ServiceName: serviceName}}
|
||||
if err := cbor.NewEncoder(stdin).Encode(req); err != nil {
|
||||
return false, err
|
||||
}
|
||||
_ = stdin.Close()
|
||||
|
||||
var resp common.AgentResponse
|
||||
if err := cbor.NewDecoder(stdout).Decode(&resp); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp.ServiceInfo == nil {
|
||||
if resp.Error != "" {
|
||||
return false, errors.New(resp.Error)
|
||||
}
|
||||
return false, errors.New("no systemd info in response")
|
||||
}
|
||||
result = resp.ServiceInfo
|
||||
return false, nil
|
||||
})
|
||||
err := sys.request(ctx, common.GetSystemdInfo, common.SystemdInfoRequest{ServiceName: serviceName}, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FetchSmartDataFromAgent fetches SMART data from the agent
|
||||
func (sys *System) FetchSmartDataFromAgent() (map[string]smart.SmartData, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
var result map[string]smart.SmartData
|
||||
err := sys.request(ctx, common.GetSmartData, nil, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -656,6 +665,9 @@ func (sys *System) createSessionWithTimeout(timeout time.Duration) (*ssh.Session
|
||||
|
||||
// closeSSHConnection closes the SSH connection but keeps the system in the manager
|
||||
func (sys *System) closeSSHConnection() {
|
||||
if sys.sshTransport != nil {
|
||||
sys.sshTransport.Close()
|
||||
}
|
||||
if sys.client != nil {
|
||||
sys.client.Close()
|
||||
sys.client = nil
|
||||
|
||||
@@ -1,54 +1,14 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// FetchSmartDataFromAgent fetches SMART data from the agent
|
||||
func (sys *System) FetchSmartDataFromAgent() (map[string]smart.SmartData, 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]smart.SmartData
|
||||
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
|
||||
}
|
||||
result = resp.SmartData
|
||||
return false, nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FetchAndSaveSmartDevices fetches SMART data from the agent and saves it to the database
|
||||
func (sys *System) FetchAndSaveSmartDevices() error {
|
||||
smartData, err := sys.FetchSmartDataFromAgent()
|
||||
|
||||
227
internal/hub/transport/ssh.go
Normal file
227
internal/hub/transport/ssh.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHTransport implements Transport over SSH connections.
|
||||
type SSHTransport struct {
|
||||
client *ssh.Client
|
||||
config *ssh.ClientConfig
|
||||
host string
|
||||
port string
|
||||
agentVersion semver.Version
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// SSHTransportConfig holds configuration for creating an SSH transport.
|
||||
type SSHTransportConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
Config *ssh.ClientConfig
|
||||
AgentVersion semver.Version
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewSSHTransport creates a new SSH transport with the given configuration.
|
||||
func NewSSHTransport(cfg SSHTransportConfig) *SSHTransport {
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 4 * time.Second
|
||||
}
|
||||
return &SSHTransport{
|
||||
config: cfg.Config,
|
||||
host: cfg.Host,
|
||||
port: cfg.Port,
|
||||
agentVersion: cfg.AgentVersion,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// SetClient sets the SSH client for reuse across requests.
|
||||
func (t *SSHTransport) SetClient(client *ssh.Client) {
|
||||
t.client = client
|
||||
}
|
||||
|
||||
// SetAgentVersion sets the agent version (extracted from SSH handshake).
|
||||
func (t *SSHTransport) SetAgentVersion(version semver.Version) {
|
||||
t.agentVersion = version
|
||||
}
|
||||
|
||||
// GetClient returns the current SSH client (for connection management).
|
||||
func (t *SSHTransport) GetClient() *ssh.Client {
|
||||
return t.client
|
||||
}
|
||||
|
||||
// GetAgentVersion returns the agent version.
|
||||
func (t *SSHTransport) GetAgentVersion() semver.Version {
|
||||
return t.agentVersion
|
||||
}
|
||||
|
||||
// Request sends a request to the agent via SSH and unmarshals the response.
|
||||
func (t *SSHTransport) Request(ctx context.Context, action common.WebSocketAction, req any, dest any) error {
|
||||
if t.client == nil {
|
||||
if err := t.connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
session, err := t.createSessionWithTimeout(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := session.Shell(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send request
|
||||
hubReq := common.HubRequest[any]{Action: action, Data: req}
|
||||
if err := cbor.NewEncoder(stdin).Encode(hubReq); err != nil {
|
||||
return fmt.Errorf("failed to encode request: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
// Read response
|
||||
var resp common.AgentResponse
|
||||
if err := cbor.NewDecoder(stdout).Decode(&resp); err != nil {
|
||||
return fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
return errors.New(resp.Error)
|
||||
}
|
||||
|
||||
if err := session.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return UnmarshalResponse(resp, action, dest)
|
||||
}
|
||||
|
||||
// IsConnected returns true if the SSH connection is active.
|
||||
func (t *SSHTransport) IsConnected() bool {
|
||||
return t.client != nil
|
||||
}
|
||||
|
||||
// Close terminates the SSH connection.
|
||||
func (t *SSHTransport) Close() {
|
||||
if t.client != nil {
|
||||
t.client.Close()
|
||||
t.client = nil
|
||||
}
|
||||
}
|
||||
|
||||
// connect establishes a new SSH connection.
|
||||
func (t *SSHTransport) connect() error {
|
||||
if t.config == nil {
|
||||
return errors.New("SSH config not set")
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
host := t.host
|
||||
if strings.HasPrefix(host, "/") {
|
||||
network = "unix"
|
||||
} else {
|
||||
host = net.JoinHostPort(host, t.port)
|
||||
}
|
||||
|
||||
client, err := ssh.Dial(network, host, t.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.client = client
|
||||
|
||||
// Extract agent version from server version string
|
||||
t.agentVersion, _ = extractAgentVersion(string(client.Conn.ServerVersion()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSessionWithTimeout creates a new SSH session with a timeout.
|
||||
func (t *SSHTransport) createSessionWithTimeout(ctx context.Context) (*ssh.Session, error) {
|
||||
if t.client == nil {
|
||||
return nil, errors.New("client not initialized")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||
defer cancel()
|
||||
|
||||
sessionChan := make(chan *ssh.Session, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
session, err := t.client.NewSession()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
} else {
|
||||
sessionChan <- session
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case session := <-sessionChan:
|
||||
return session, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("timeout creating session")
|
||||
}
|
||||
}
|
||||
|
||||
// extractAgentVersion extracts the beszel version from SSH server version string.
|
||||
func extractAgentVersion(versionString string) (semver.Version, error) {
|
||||
_, after, _ := strings.Cut(versionString, "_")
|
||||
return semver.Parse(after)
|
||||
}
|
||||
|
||||
// RequestWithRetry sends a request with automatic retry on connection failures.
|
||||
func (t *SSHTransport) RequestWithRetry(ctx context.Context, action common.WebSocketAction, req any, dest any, retries int) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= retries; attempt++ {
|
||||
err := t.Request(ctx, action, req, dest)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
// Check if it's a connection error that warrants a retry
|
||||
if isConnectionError(err) && attempt < retries {
|
||||
t.Close()
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// isConnectionError checks if an error indicates a connection problem.
|
||||
func isConnectionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errStr := err.Error()
|
||||
return strings.Contains(errStr, "connection") ||
|
||||
strings.Contains(errStr, "EOF") ||
|
||||
strings.Contains(errStr, "closed") ||
|
||||
errors.Is(err, io.EOF)
|
||||
}
|
||||
112
internal/hub/transport/transport.go
Normal file
112
internal/hub/transport/transport.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Package transport provides a unified abstraction for hub-agent communication
|
||||
// over different transports (WebSocket, SSH).
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
)
|
||||
|
||||
// Transport defines the interface for hub-agent communication.
|
||||
// Both WebSocket and SSH transports implement this interface.
|
||||
type Transport interface {
|
||||
// Request sends a request to the agent and unmarshals the response into dest.
|
||||
// The dest parameter should be a pointer to the expected response type.
|
||||
Request(ctx context.Context, action common.WebSocketAction, req any, dest any) error
|
||||
// IsConnected returns true if the transport connection is active.
|
||||
IsConnected() bool
|
||||
// Close terminates the transport connection.
|
||||
Close()
|
||||
}
|
||||
|
||||
// UnmarshalResponse unmarshals an AgentResponse into the destination type.
|
||||
// It first checks the generic Data field (0.19+ agents), then falls back
|
||||
// to legacy typed fields for backward compatibility with 0.18.0 agents.
|
||||
func UnmarshalResponse(resp common.AgentResponse, action common.WebSocketAction, dest any) error {
|
||||
if dest == nil {
|
||||
return errors.New("nil destination")
|
||||
}
|
||||
// Try generic Data field first (0.19+)
|
||||
if len(resp.Data) > 0 {
|
||||
if err := cbor.Unmarshal(resp.Data, dest); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal generic response data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Fall back to legacy typed fields for older agents/hubs.
|
||||
return unmarshalLegacyResponse(resp, action, dest)
|
||||
}
|
||||
|
||||
// unmarshalLegacyResponse handles legacy responses that use typed fields.
|
||||
func unmarshalLegacyResponse(resp common.AgentResponse, action common.WebSocketAction, dest any) error {
|
||||
switch action {
|
||||
case common.GetData:
|
||||
d, ok := dest.(*system.CombinedData)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for GetData: %T", dest)
|
||||
}
|
||||
if resp.SystemData == nil {
|
||||
return errors.New("no system data in response")
|
||||
}
|
||||
*d = *resp.SystemData
|
||||
return nil
|
||||
case common.CheckFingerprint:
|
||||
d, ok := dest.(*common.FingerprintResponse)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for CheckFingerprint: %T", dest)
|
||||
}
|
||||
if resp.Fingerprint == nil {
|
||||
return errors.New("no fingerprint in response")
|
||||
}
|
||||
*d = *resp.Fingerprint
|
||||
return nil
|
||||
case common.GetContainerLogs:
|
||||
d, ok := dest.(*string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for GetContainerLogs: %T", dest)
|
||||
}
|
||||
if resp.String == nil {
|
||||
return errors.New("no logs in response")
|
||||
}
|
||||
*d = *resp.String
|
||||
return nil
|
||||
case common.GetContainerInfo:
|
||||
d, ok := dest.(*string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for GetContainerInfo: %T", dest)
|
||||
}
|
||||
if resp.String == nil {
|
||||
return errors.New("no info in response")
|
||||
}
|
||||
*d = *resp.String
|
||||
return nil
|
||||
case common.GetSmartData:
|
||||
d, ok := dest.(*map[string]smart.SmartData)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for GetSmartData: %T", dest)
|
||||
}
|
||||
if resp.SmartData == nil {
|
||||
return errors.New("no SMART data in response")
|
||||
}
|
||||
*d = resp.SmartData
|
||||
return nil
|
||||
case common.GetSystemdInfo:
|
||||
d, ok := dest.(*systemd.ServiceDetails)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for GetSystemdInfo: %T", dest)
|
||||
}
|
||||
if resp.ServiceInfo == nil {
|
||||
return errors.New("no systemd info in response")
|
||||
}
|
||||
*d = resp.ServiceInfo
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported action: %d", action)
|
||||
}
|
||||
74
internal/hub/transport/websocket.go
Normal file
74
internal/hub/transport/websocket.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
)
|
||||
|
||||
// ErrWebSocketNotConnected indicates a WebSocket transport is not currently connected.
|
||||
var ErrWebSocketNotConnected = errors.New("websocket not connected")
|
||||
|
||||
// WebSocketTransport implements Transport over WebSocket connections.
|
||||
type WebSocketTransport struct {
|
||||
wsConn *ws.WsConn
|
||||
}
|
||||
|
||||
// NewWebSocketTransport creates a new WebSocket transport wrapper.
|
||||
func NewWebSocketTransport(wsConn *ws.WsConn) *WebSocketTransport {
|
||||
return &WebSocketTransport{wsConn: wsConn}
|
||||
}
|
||||
|
||||
// Request sends a request to the agent via WebSocket and unmarshals the response.
|
||||
func (t *WebSocketTransport) Request(ctx context.Context, action common.WebSocketAction, req any, dest any) error {
|
||||
if !t.IsConnected() {
|
||||
return ErrWebSocketNotConnected
|
||||
}
|
||||
|
||||
pendingReq, err := t.wsConn.SendRequest(ctx, action, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for response
|
||||
select {
|
||||
case message := <-pendingReq.ResponseCh:
|
||||
defer message.Close()
|
||||
defer pendingReq.Cancel()
|
||||
|
||||
// Legacy agents (< MinVersionAgentResponse) respond with a raw payload instead of an AgentResponse wrapper.
|
||||
if t.wsConn.AgentVersion().LT(beszel.MinVersionAgentResponse) {
|
||||
return cbor.Unmarshal(message.Data.Bytes(), dest)
|
||||
}
|
||||
|
||||
var agentResponse common.AgentResponse
|
||||
if err := cbor.Unmarshal(message.Data.Bytes(), &agentResponse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if agentResponse.Error != "" {
|
||||
return errors.New(agentResponse.Error)
|
||||
}
|
||||
|
||||
return UnmarshalResponse(agentResponse, action, dest)
|
||||
|
||||
case <-pendingReq.Context.Done():
|
||||
return pendingReq.Context.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// IsConnected returns true if the WebSocket connection is active.
|
||||
func (t *WebSocketTransport) IsConnected() bool {
|
||||
return t.wsConn != nil && t.wsConn.IsConnected()
|
||||
}
|
||||
|
||||
// Close terminates the WebSocket connection.
|
||||
func (t *WebSocketTransport) Close() {
|
||||
if t.wsConn != nil {
|
||||
t.wsConn.Close(nil)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ func Update(cmd *cobra.Command, _ []string) {
|
||||
fmt.Printf("Warning: failed to set executable permissions: %v\n", err)
|
||||
}
|
||||
|
||||
// Fix SELinux context if necessary
|
||||
if err := ghupdate.HandleSELinuxContext(exePath); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: SELinux context handling: %v", err)
|
||||
}
|
||||
|
||||
// Try to restart the service if it's running
|
||||
restartService()
|
||||
}
|
||||
|
||||
@@ -6,14 +6,12 @@ import (
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
"github.com/lxzan/gws"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ResponseHandler defines interface for handling agent responses
|
||||
// ResponseHandler defines interface for handling agent responses.
|
||||
// This is used by handleAgentRequest for legacy response handling.
|
||||
type ResponseHandler interface {
|
||||
Handle(agentResponse common.AgentResponse) error
|
||||
HandleLegacy(rawData []byte) error
|
||||
@@ -27,167 +25,7 @@ func (h *BaseHandler) HandleLegacy(rawData []byte) error {
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// systemDataHandler implements ResponseHandler for system data requests
|
||||
type systemDataHandler struct {
|
||||
data *system.CombinedData
|
||||
}
|
||||
|
||||
func (h *systemDataHandler) HandleLegacy(rawData []byte) error {
|
||||
return cbor.Unmarshal(rawData, h.data)
|
||||
}
|
||||
|
||||
func (h *systemDataHandler) Handle(agentResponse common.AgentResponse) error {
|
||||
if agentResponse.SystemData != nil {
|
||||
*h.data = *agentResponse.SystemData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestSystemData requests system metrics from the agent and unmarshals the response.
|
||||
func (ws *WsConn) RequestSystemData(ctx context.Context, data *system.CombinedData, options common.DataRequestOptions) error {
|
||||
if !ws.IsConnected() {
|
||||
return gws.ErrConnClosed
|
||||
}
|
||||
|
||||
req, err := ws.requestManager.SendRequest(ctx, common.GetData, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := &systemDataHandler{data: data}
|
||||
return ws.handleAgentRequest(req, handler)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// RequestSystemdInfo requests detailed information about a systemd service via WebSocket.
|
||||
func (ws *WsConn) RequestSystemdInfo(ctx context.Context, serviceName string) (systemd.ServiceDetails, error) {
|
||||
if !ws.IsConnected() {
|
||||
return nil, gws.ErrConnClosed
|
||||
}
|
||||
|
||||
req, err := ws.requestManager.SendRequest(ctx, common.GetSystemdInfo, common.SystemdInfoRequest{ServiceName: serviceName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result systemd.ServiceDetails
|
||||
handler := &systemdInfoHandler{result: &result}
|
||||
if err := ws.handleAgentRequest(req, handler); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// systemdInfoHandler parses ServiceDetails from AgentResponse
|
||||
type systemdInfoHandler struct {
|
||||
BaseHandler
|
||||
result *systemd.ServiceDetails
|
||||
}
|
||||
|
||||
func (h *systemdInfoHandler) Handle(agentResponse common.AgentResponse) error {
|
||||
if agentResponse.ServiceInfo == nil {
|
||||
return errors.New("no systemd info in response")
|
||||
}
|
||||
*h.result = agentResponse.ServiceInfo
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// RequestSmartData requests SMART data via WebSocket.
|
||||
func (ws *WsConn) RequestSmartData(ctx context.Context) (map[string]smart.SmartData, 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]smart.SmartData
|
||||
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]smart.SmartData
|
||||
}
|
||||
|
||||
func (h *smartDataHandler) Handle(agentResponse common.AgentResponse) error {
|
||||
if agentResponse.SmartData == nil {
|
||||
return errors.New("no SMART data in response")
|
||||
}
|
||||
*h.result = agentResponse.SmartData
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Fingerprint handling (used for WebSocket authentication)
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// fingerprintHandler implements ResponseHandler for fingerprint requests
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
//go:build testing
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSystemdInfoHandlerSuccess(t *testing.T) {
|
||||
handler := &systemdInfoHandler{
|
||||
result: &systemd.ServiceDetails{},
|
||||
}
|
||||
|
||||
// Test successful handling with valid ServiceInfo
|
||||
testDetails := systemd.ServiceDetails{
|
||||
"Id": "nginx.service",
|
||||
"ActiveState": "active",
|
||||
"SubState": "running",
|
||||
"Description": "A high performance web server",
|
||||
"ExecMainPID": 1234,
|
||||
"MemoryCurrent": 1024000,
|
||||
}
|
||||
|
||||
response := common.AgentResponse{
|
||||
ServiceInfo: testDetails,
|
||||
}
|
||||
|
||||
err := handler.Handle(response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testDetails, *handler.result)
|
||||
}
|
||||
|
||||
func TestSystemdInfoHandlerError(t *testing.T) {
|
||||
handler := &systemdInfoHandler{
|
||||
result: &systemd.ServiceDetails{},
|
||||
}
|
||||
|
||||
// Test error handling when ServiceInfo is nil
|
||||
response := common.AgentResponse{
|
||||
ServiceInfo: nil,
|
||||
Error: "service not found",
|
||||
}
|
||||
|
||||
err := handler.Handle(response)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "no systemd info in response", err.Error())
|
||||
}
|
||||
|
||||
func TestSystemdInfoHandlerEmptyResponse(t *testing.T) {
|
||||
handler := &systemdInfoHandler{
|
||||
result: &systemd.ServiceDetails{},
|
||||
}
|
||||
|
||||
// Test with completely empty response
|
||||
response := common.AgentResponse{}
|
||||
|
||||
err := handler.Handle(response)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "no systemd info in response", err.Error())
|
||||
}
|
||||
|
||||
func TestSystemdInfoHandlerLegacyNotSupported(t *testing.T) {
|
||||
handler := &systemdInfoHandler{
|
||||
result: &systemd.ServiceDetails{},
|
||||
}
|
||||
|
||||
// Test that legacy format is not supported
|
||||
err := handler.HandleLegacy([]byte("some data"))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "legacy format not supported", err.Error())
|
||||
}
|
||||
@@ -45,7 +45,15 @@ func NewRequestManager(conn *gws.Conn) *RequestManager {
|
||||
func (rm *RequestManager) SendRequest(ctx context.Context, action common.WebSocketAction, data any) (*PendingRequest, error) {
|
||||
reqID := RequestID(rm.nextID.Add(1))
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// Respect any caller-provided deadline. If none is set, apply a reasonable default
|
||||
// so pending requests don't live forever if the agent never responds.
|
||||
reqCtx := ctx
|
||||
var cancel context.CancelFunc
|
||||
if _, hasDeadline := ctx.Deadline(); hasDeadline {
|
||||
reqCtx, cancel = context.WithCancel(ctx)
|
||||
} else {
|
||||
reqCtx, cancel = context.WithTimeout(ctx, 5*time.Second)
|
||||
}
|
||||
|
||||
req := &PendingRequest{
|
||||
ID: reqID,
|
||||
@@ -100,6 +108,11 @@ func (rm *RequestManager) handleResponse(message *gws.Message) {
|
||||
return
|
||||
}
|
||||
|
||||
if response.Id == nil {
|
||||
rm.routeLegacyResponse(message)
|
||||
return
|
||||
}
|
||||
|
||||
reqID := RequestID(*response.Id)
|
||||
|
||||
rm.RLock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"weak"
|
||||
@@ -161,3 +162,14 @@ func (ws *WsConn) handleAgentRequest(req *PendingRequest, handler ResponseHandle
|
||||
func (ws *WsConn) IsConnected() bool {
|
||||
return ws.conn != nil
|
||||
}
|
||||
|
||||
// AgentVersion returns the connected agent's version (as reported during handshake).
|
||||
func (ws *WsConn) AgentVersion() semver.Version {
|
||||
return ws.agentVersion
|
||||
}
|
||||
|
||||
// SendRequest sends a request to the agent and returns a pending request handle.
|
||||
// This is used by the transport layer to send requests.
|
||||
func (ws *WsConn) SendRequest(ctx context.Context, action common.WebSocketAction, data any) (*PendingRequest, error) {
|
||||
return ws.requestManager.SendRequest(ctx, action, data)
|
||||
}
|
||||
|
||||
@@ -184,14 +184,18 @@ func TestCommonActions(t *testing.T) {
|
||||
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"}
|
||||
func TestFingerprintHandler(t *testing.T) {
|
||||
var result common.FingerprintResponse
|
||||
h := &fingerprintHandler{result: &result}
|
||||
|
||||
logValue := "test logs"
|
||||
resp := common.AgentResponse{String: &logValue}
|
||||
resp := common.AgentResponse{Fingerprint: &common.FingerprintResponse{
|
||||
Fingerprint: "test-fingerprint",
|
||||
Hostname: "test-host",
|
||||
}}
|
||||
err := h.Handle(resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, logValue, h.value)
|
||||
assert.Equal(t, "test-fingerprint", result.Fingerprint)
|
||||
assert.Equal(t, "test-host", result.Hostname)
|
||||
}
|
||||
|
||||
// TestHandler tests that we can create a Handler
|
||||
|
||||
@@ -1617,6 +1617,74 @@ func init() {
|
||||
"type": "base",
|
||||
"updateRule": "",
|
||||
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
|
||||
},
|
||||
{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{10}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 10,
|
||||
"min": 10,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1597481275",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "token",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_3383022248",
|
||||
"indexes": [
|
||||
"CREATE INDEX ` + "`" + `idx_iaD9Y2Lgbl` + "`" + ` ON ` + "`" + `universal_tokens` + "`" + ` (` + "`" + `token` + "`" + `)",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `idx_wdR0A4PbRG` + "`" + ` ON ` + "`" + `universal_tokens` + "`" + ` (` + "`" + `user` + "`" + `)"
|
||||
],
|
||||
"listRule": null,
|
||||
"name": "universal_tokens",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}
|
||||
]`
|
||||
|
||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
||||
"he",
|
||||
"hr",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
|
||||
4
internal/site/package-lock.json
generated
4
internal/site/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "beszel",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.2",
|
||||
"dependencies": {
|
||||
"@henrygd/queue": "^1.0.7",
|
||||
"@henrygd/semaphore": "^0.0.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"private": true,
|
||||
"version": "0.18.0-beta.1",
|
||||
"version": "0.18.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -77,4 +77,4 @@
|
||||
"optionalDependencies": {
|
||||
"@esbuild/linux-arm64": "^0.21.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export const ActiveAlerts = () => {
|
||||
>
|
||||
<info.icon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
{systems[alert.system]?.name} {info.name()}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{alert.name === "Status" ? (
|
||||
|
||||
@@ -49,10 +49,12 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className={cn("flex gap-1 max-xs:h-[2.4rem]", className)}>
|
||||
<PlusIcon className="h-4 w-4 -ms-1" />
|
||||
<Trans>
|
||||
Add <span className="hidden sm:inline">System</span>
|
||||
</Trans>
|
||||
<PlusIcon className="h-4 w-4 450:-ms-1" />
|
||||
<span className="hidden 450:inline">
|
||||
<Trans>
|
||||
Add <span className="hidden sm:inline">System</span>
|
||||
</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{opened.current && <SystemDialog setOpen={setOpen} />}
|
||||
|
||||
@@ -57,8 +57,13 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
|
||||
.then(
|
||||
({ items }) => {
|
||||
if (items.length === 0) {
|
||||
setData([]);
|
||||
return;
|
||||
setData((curItems) => {
|
||||
if (systemId) {
|
||||
return curItems?.filter((item) => item.system !== systemId) ?? []
|
||||
}
|
||||
return []
|
||||
})
|
||||
return
|
||||
}
|
||||
setData((curItems) => {
|
||||
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
|
||||
@@ -280,7 +285,7 @@ async function getInfoHtml(container: ContainerRecord): Promise<string> {
|
||||
])
|
||||
try {
|
||||
info = JSON.stringify(JSON.parse(info), null, 2)
|
||||
} catch (_) {}
|
||||
} catch (_) { }
|
||||
return info ? highlighter.codeToHtml(info, { lang: "json", theme: syntaxTheme }) : t`No results.`
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -337,7 +342,7 @@ function ContainerSheet({
|
||||
setLogsDisplay("")
|
||||
setInfoDisplay("")
|
||||
if (!container) return
|
||||
;(async () => {
|
||||
; (async () => {
|
||||
const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])
|
||||
setLogsDisplay(logsHtml)
|
||||
setInfoDisplay(infoHtml)
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { LanguagesIcon } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import languages from "@/lib/languages"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||
|
||||
export function LangToggle() {
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const LangTrans = <Trans>Language</Trans>
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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>
|
||||
<DropdownMenuTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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">{LangTrans}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{LangTrans}</TooltipContent>
|
||||
</Tooltip>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="grid grid-cols-3">
|
||||
{languages.map(({ lang, label, e }) => (
|
||||
{languages.map(([lang, label, e]) => (
|
||||
<DropdownMenuItem
|
||||
key={lang}
|
||||
className={cn("px-2.5 flex gap-2.5 cursor-pointer", lang === i18n.locale && "bg-accent/70 font-medium")}
|
||||
|
||||
@@ -25,13 +25,13 @@ const passwordSchema = v.pipe(
|
||||
)
|
||||
|
||||
const LoginSchema = v.looseObject({
|
||||
name: honeypot,
|
||||
website: honeypot,
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
})
|
||||
|
||||
const RegisterSchema = v.looseObject({
|
||||
name: honeypot,
|
||||
website: honeypot,
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
passwordConfirm: passwordSchema,
|
||||
@@ -248,8 +248,19 @@ export function UserAuthForm({
|
||||
)}
|
||||
<div className="sr-only">
|
||||
{/* honeypot */}
|
||||
<label htmlFor="name"></label>
|
||||
<input id="name" type="text" name="name" tabIndex={-1} autoComplete="off" />
|
||||
<label htmlFor="website"></label>
|
||||
<input
|
||||
id="website"
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-bwignore
|
||||
data-form-type="other"
|
||||
data-protonpass-ignore
|
||||
/>
|
||||
</div>
|
||||
<button className={cn(buttonVariants())} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -2,19 +2,28 @@ import { t } from "@lingui/core/macro"
|
||||
import { MoonStarIcon, SunIcon } from "lucide-react"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size="icon"
|
||||
aria-label={t`Toggle theme`}
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
>
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" />
|
||||
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size="icon"
|
||||
aria-label={t`Toggle theme`}
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
>
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" />
|
||||
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>Toggle theme</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { LangToggle } from "./lang-toggle"
|
||||
import { Logo } from "./logo"
|
||||
import { ModeToggle } from "./mode-toggle"
|
||||
import { $router, basePath, Link, prependBasePath } from "./router"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
|
||||
|
||||
const CommandPalette = lazy(() => import("./command-palette"))
|
||||
|
||||
@@ -49,30 +49,52 @@ export default function Navbar() {
|
||||
</Link>
|
||||
<SearchButton />
|
||||
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}
|
||||
<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>
|
||||
<Link
|
||||
href={getPagePath($router, "smart")}
|
||||
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
aria-label="S.M.A.R.T."
|
||||
>
|
||||
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
||||
</Link>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>All Containers</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={getPagePath($router, "smart")}
|
||||
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
aria-label="S.M.A.R.T."
|
||||
>
|
||||
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>S.M.A.R.T.</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<LangToggle />
|
||||
<ModeToggle />
|
||||
<Link
|
||||
href={getPagePath($router, "settings", { name: "general" })}
|
||||
aria-label="Settings"
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
>
|
||||
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</Link>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={getPagePath($router, "settings", { name: "general" })}
|
||||
aria-label="Settings"
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
>
|
||||
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<Trans>Settings</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button aria-label="User Actions" className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}>
|
||||
@@ -129,21 +151,21 @@ export default function Navbar() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AddSystemButton className="ms-2 hidden 450:flex" />
|
||||
<AddSystemButton className="ms-2" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Kbd = ({ children }: { children: React.ReactNode }) => (
|
||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||
{children}
|
||||
</kbd>
|
||||
)
|
||||
|
||||
function SearchButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const Kbd = ({ children }: { children: React.ReactNode }) => (
|
||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||
{children}
|
||||
</kbd>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -68,10 +68,10 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.lang} value={lang.lang}>
|
||||
<span className="me-2.5">{lang.e}</span>
|
||||
{lang.label}
|
||||
{languages.map(([lang, label, e]) => (
|
||||
<SelectItem key={lang} value={lang}>
|
||||
<span className="me-2.5">{e}</span>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
@@ -137,21 +138,23 @@ const SectionUniversalToken = memo(() => {
|
||||
const [token, setToken] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [checked, setChecked] = useState(false)
|
||||
const [isPermanent, setIsPermanent] = useState(false)
|
||||
|
||||
async function updateToken(enable: number = -1) {
|
||||
async function updateToken(enable: number = -1, permanent: number = -1) {
|
||||
// enable: 0 for disable, 1 for enable, -1 (unset) for get current state
|
||||
const data = await pb.send(`/api/beszel/universal-token`, {
|
||||
query: {
|
||||
token,
|
||||
enable,
|
||||
permanent,
|
||||
},
|
||||
})
|
||||
setToken(data.token)
|
||||
setChecked(data.active)
|
||||
setIsPermanent(!!data.permanent)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only on mount
|
||||
useEffect(() => {
|
||||
updateToken()
|
||||
}, [])
|
||||
@@ -162,30 +165,64 @@ const SectionUniversalToken = memo(() => {
|
||||
<Trans>Universal token</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>
|
||||
When enabled, this token allows agents to self-register without prior system creation. Expires after one hour
|
||||
or on hub restart.
|
||||
</Trans>
|
||||
<Trans>When enabled, this token allows agents to self-register without prior system creation.</Trans>
|
||||
</p>
|
||||
<div className="min-h-16 overflow-auto max-w-full inline-flex items-center gap-5 mt-3 border py-2 ps-5 pe-4 rounded-md">
|
||||
<div className="mt-3 border rounded-md px-4 py-3 max-w-full">
|
||||
{!isLoading && (
|
||||
<>
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onCheckedChange={(checked) => {
|
||||
updateToken(checked ? 1 : 0)
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm text-primary opacity-60 transition-opacity",
|
||||
checked ? "opacity-100" : "select-none"
|
||||
)}
|
||||
>
|
||||
{token}
|
||||
</span>
|
||||
<ActionsButtonUniversalToken token={token} checked={checked} />
|
||||
</>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<Switch
|
||||
checked={checked}
|
||||
onCheckedChange={(checked) => {
|
||||
// Keep current permanence preference when enabling/disabling
|
||||
updateToken(checked ? 1 : 0, isPermanent ? 1 : 0)
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-0 flex-1 overflow-auto">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm text-primary opacity-60 transition-opacity",
|
||||
checked ? "opacity-100" : "select-none"
|
||||
)}
|
||||
>
|
||||
{token}
|
||||
</span>
|
||||
</div>
|
||||
<ActionsButtonUniversalToken token={token} checked={checked} />
|
||||
</div>
|
||||
|
||||
{checked && (
|
||||
<div className="border-t pt-3">
|
||||
<div className="text-sm font-medium">
|
||||
<Trans>Persistence</Trans>
|
||||
</div>
|
||||
<Tabs
|
||||
value={isPermanent ? "permanent" : "ephemeral"}
|
||||
onValueChange={(value) => updateToken(1, value === "permanent" ? 1 : 0)}
|
||||
className="mt-2"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger className="xs:min-w-40" value="ephemeral">
|
||||
<Trans>Ephemeral</Trans>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="xs:min-w-40" value="permanent">
|
||||
<Trans>Permanent</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="ephemeral" className="mt-3">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Expires after one hour or on hub restart.</Trans>
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="permanent" className="mt-3">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Saved in the database and does not expire until you disable it.</Trans>
|
||||
</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import MemChart from "@/components/charts/mem-chart"
|
||||
import SwapChart from "@/components/charts/swap-chart"
|
||||
import TemperatureChart from "@/components/charts/temperature-chart"
|
||||
import { getPbTimestamp, pb } from "@/lib/api"
|
||||
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums"
|
||||
import { ChartType, SystemStatus, Unit } from "@/lib/enums"
|
||||
import { batteryStateTranslations } from "@/lib/i18n"
|
||||
import {
|
||||
$allSystemsById,
|
||||
@@ -222,7 +222,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
}, [system.id])
|
||||
|
||||
// subscribe to realtime metrics if chart time is 1m
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||
useEffect(() => {
|
||||
let unsub = () => {}
|
||||
if (!system.id || chartTime !== "1m") {
|
||||
@@ -260,7 +259,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
}
|
||||
}, [chartTime, system.id])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||
const chartData: ChartData = useMemo(() => {
|
||||
const lastCreated = Math.max(
|
||||
(systemStats.at(-1)?.created as number) ?? 0,
|
||||
@@ -300,7 +298,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
}, [])
|
||||
|
||||
// get stats
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
|
||||
useEffect(() => {
|
||||
if (!system.id || !chartTime || chartTime === "1m") {
|
||||
return
|
||||
@@ -403,11 +400,32 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
const containerFilterBar = containerData.length ? <FilterBar /> : null
|
||||
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
||||
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
|
||||
const hasGpuData = lastGpuVals.length > 0
|
||||
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined || gpu.pp !== undefined)
|
||||
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
|
||||
const isLinux = (details?.os ?? system.info?.os) === Os.Linux
|
||||
const lastGpus = systemStats.at(-1)?.stats?.g
|
||||
|
||||
let hasGpuData = false
|
||||
let hasGpuEnginesData = false
|
||||
let hasGpuPowerData = false
|
||||
|
||||
if (lastGpus) {
|
||||
// check if there are any GPUs at all
|
||||
hasGpuData = Object.keys(lastGpus).length > 0
|
||||
// check if there are any GPUs with engines or power data
|
||||
for (let i = 0; i < systemStats.length && (!hasGpuEnginesData || !hasGpuPowerData); i++) {
|
||||
const gpus = systemStats[i].stats?.g
|
||||
if (!gpus) continue
|
||||
for (const id in gpus) {
|
||||
if (!hasGpuEnginesData && gpus[id].e !== undefined) {
|
||||
hasGpuEnginesData = true
|
||||
}
|
||||
if (!hasGpuPowerData && (gpus[id].p !== undefined || gpus[id].pp !== undefined)) {
|
||||
hasGpuPowerData = true
|
||||
}
|
||||
if (hasGpuEnginesData && hasGpuPowerData) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isLinux = !(details?.os ?? system.info?.os)
|
||||
const isPodman = details?.podman ?? system.info?.p ?? false
|
||||
|
||||
return (
|
||||
@@ -718,64 +736,65 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
<GpuEnginesChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
)}
|
||||
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
|
||||
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData
|
||||
return (
|
||||
<div key={id} className="contents">
|
||||
<ChartCard
|
||||
className={cn(grid && "!col-span-1")}
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${gpu.n} ${t`Usage`}`}
|
||||
description={t`Average utilization of ${gpu.n}`}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Usage`,
|
||||
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
|
||||
color: 1,
|
||||
opacity: 0.35,
|
||||
},
|
||||
]}
|
||||
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
|
||||
contentFormatter={({ value }) => `${decimalString(value)}%`}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
{(gpu.mt ?? 0) > 0 && (
|
||||
{lastGpus &&
|
||||
Object.keys(lastGpus).map((id) => {
|
||||
const gpu = lastGpus[id] as GPUData
|
||||
return (
|
||||
<div key={id} className="contents">
|
||||
<ChartCard
|
||||
className={cn(grid && "!col-span-1")}
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${gpu.n} VRAM`}
|
||||
description={t`Precise utilization at the recorded time`}
|
||||
title={`${gpu.n} ${t`Usage`}`}
|
||||
description={t`Average utilization of ${gpu.n}`}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Usage`,
|
||||
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
|
||||
color: 2,
|
||||
opacity: 0.25,
|
||||
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
|
||||
color: 1,
|
||||
opacity: 0.35,
|
||||
},
|
||||
]}
|
||||
max={gpu.mt}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
|
||||
return `${decimalString(convertedValue)} ${unit}`
|
||||
}}
|
||||
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
|
||||
contentFormatter={({ value }) => `${decimalString(value)}%`}
|
||||
/>
|
||||
</ChartCard>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{(gpu.mt ?? 0) > 0 && (
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={`${gpu.n} VRAM`}
|
||||
description={t`Precise utilization at the recorded time`}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Usage`,
|
||||
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
|
||||
color: 2,
|
||||
opacity: 0.25,
|
||||
},
|
||||
]}
|
||||
max={gpu.mt}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
|
||||
return `${decimalString(convertedValue)} ${unit}`
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -869,16 +888,30 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
})
|
||||
|
||||
function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
|
||||
const dataPoints: DataPoint[] = []
|
||||
const engines = Object.keys(chartData.systemStats?.at(-1)?.stats.g?.[0]?.e ?? {}).sort()
|
||||
for (const engine of engines) {
|
||||
dataPoints.push({
|
||||
label: engine,
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.g?.[0]?.e?.[engine] ?? 0,
|
||||
color: `hsl(${140 + (((engines.indexOf(engine) * 360) / engines.length) % 360)}, 65%, 52%)`,
|
||||
opacity: 0.35,
|
||||
})
|
||||
const { gpuId, engines } = useMemo(() => {
|
||||
for (let i = chartData.systemStats.length - 1; i >= 0; i--) {
|
||||
const gpus = chartData.systemStats[i].stats?.g
|
||||
if (!gpus) continue
|
||||
for (const id in gpus) {
|
||||
if (gpus[id].e) {
|
||||
return { gpuId: id, engines: Object.keys(gpus[id].e).sort() }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { gpuId: null, engines: [] }
|
||||
}, [chartData.systemStats])
|
||||
|
||||
if (!gpuId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const dataPoints: DataPoint[] = engines.map((engine, i) => ({
|
||||
label: engine,
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.g?.[gpuId]?.e?.[engine] ?? 0,
|
||||
color: `hsl(${140 + (((i * 360) / engines.length) % 360)}, 65%, 52%)`,
|
||||
opacity: 0.35,
|
||||
}))
|
||||
|
||||
return (
|
||||
<LineChartDefault
|
||||
legend={true}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
|
||||
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
|
||||
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
|
||||
@@ -135,43 +135,41 @@ export default function InfoBar({
|
||||
<div>
|
||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="capitalize flex gap-2 items-center">
|
||||
<span className={cn("relative flex h-3 w-3")}>
|
||||
{system.status === SystemStatus.Up && (
|
||||
<span
|
||||
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||
style={{ animationDuration: "1.5s" }}
|
||||
></span>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="capitalize flex gap-2 items-center">
|
||||
<span className={cn("relative flex h-3 w-3")}>
|
||||
{system.status === SystemStatus.Up && (
|
||||
<span
|
||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||
"bg-green-500": system.status === SystemStatus.Up,
|
||||
"bg-red-500": system.status === SystemStatus.Down,
|
||||
"bg-primary/40": system.status === SystemStatus.Paused,
|
||||
"bg-yellow-500": system.status === SystemStatus.Pending,
|
||||
})}
|
||||
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||
style={{ animationDuration: "1.5s" }}
|
||||
></span>
|
||||
</span>
|
||||
{translatedStatus}
|
||||
)}
|
||||
<span
|
||||
className={cn("relative inline-flex rounded-full h-3 w-3", {
|
||||
"bg-green-500": system.status === SystemStatus.Up,
|
||||
"bg-red-500": system.status === SystemStatus.Down,
|
||||
"bg-primary/40": system.status === SystemStatus.Paused,
|
||||
"bg-yellow-500": system.status === SystemStatus.Pending,
|
||||
})}
|
||||
></span>
|
||||
</span>
|
||||
{translatedStatus}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{system.info.ct && (
|
||||
<TooltipContent>
|
||||
<div className="flex gap-1 items-center">
|
||||
{system.info.ct === ConnectionType.WebSocket ? (
|
||||
<WebSocketIcon className="size-4" />
|
||||
) : (
|
||||
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
||||
)}
|
||||
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{system.info.ct && (
|
||||
<TooltipContent>
|
||||
<div className="flex gap-1 items-center">
|
||||
{system.info.ct === ConnectionType.WebSocket ? (
|
||||
<WebSocketIcon className="size-4" />
|
||||
) : (
|
||||
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
|
||||
)}
|
||||
{connectionTypeLabels[system.info.ct as ConnectionType]}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
{systemInfo.map(({ value, label, Icon, hide }) => {
|
||||
if (hide || !value) {
|
||||
@@ -186,12 +184,10 @@ export default function InfoBar({
|
||||
<div key={value} className="contents">
|
||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||
{label ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={150}>
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
@@ -202,26 +198,24 @@ export default function InfoBar({
|
||||
</div>
|
||||
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
|
||||
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t`Toggle grid`}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden xl:flex p-0 text-primary"
|
||||
onClick={() => setGrid(!grid)}
|
||||
>
|
||||
{grid ? (
|
||||
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
||||
) : (
|
||||
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label={t`Toggle grid`}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden xl:flex p-0 text-primary"
|
||||
onClick={() => setGrid(!grid)}
|
||||
>
|
||||
{grid ? (
|
||||
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
|
||||
) : (
|
||||
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t`Toggle grid`}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -128,17 +128,32 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
|
||||
cell: (info) => {
|
||||
const { name, id } = info.row.original
|
||||
const longestName = useStore($longestSystemNameLen)
|
||||
const linkUrl = getPagePath($router, "system", { id })
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1">
|
||||
<IndicatorDot system={info.row.original} />
|
||||
{/* NOTE: change to 1 ch if switching to monospace font */}
|
||||
<span className="truncate" style={{ width: `${longestName / 1.1}ch` }}>
|
||||
<Link
|
||||
href={linkUrl}
|
||||
tabIndex={-1}
|
||||
className="truncate z-10 relative"
|
||||
style={{ width: `${longestName / 1.05}ch` }}
|
||||
onMouseEnter={(e) => {
|
||||
// set title on hover if text is truncated to show full name
|
||||
const a = e.currentTarget
|
||||
if (a.scrollWidth > a.clientWidth) {
|
||||
a.title = name
|
||||
} else {
|
||||
a.removeAttribute("title")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</Link>
|
||||
</span>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { id })}
|
||||
href={linkUrl}
|
||||
className="inset-0 absolute size-full"
|
||||
aria-label={name}
|
||||
></Link>
|
||||
@@ -287,12 +302,12 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
|
||||
return null
|
||||
}
|
||||
|
||||
const iconColor = pct < 10 ? "text-red-500" : pct < 25 ? "text-yellow-500" : "text-muted-foreground"
|
||||
|
||||
let Icon = PlugChargingIcon
|
||||
let iconColor = "text-muted-foreground"
|
||||
|
||||
if (state !== BatteryState.Charging) {
|
||||
if (pct < 25) {
|
||||
iconColor = pct < 11 ? "text-red-500" : "text-yellow-500"
|
||||
Icon = BatteryLowIcon
|
||||
} else if (pct < 75) {
|
||||
Icon = BatteryMediumIcon
|
||||
@@ -439,9 +454,9 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
|
||||
const meterClass = cn(
|
||||
"h-full",
|
||||
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
|
||||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
|
||||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
|
||||
STATUS_COLORS.down
|
||||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
|
||||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
|
||||
STATUS_COLORS.down
|
||||
)
|
||||
return (
|
||||
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
|
||||
@@ -553,7 +568,7 @@ export function IndicatorDot({ system, className }: { system: SystemRecord; clas
|
||||
return (
|
||||
<span
|
||||
className={cn("shrink-0 size-2 rounded-full", className)}
|
||||
// style={{ marginBottom: "-1px" }}
|
||||
// style={{ marginBottom: "-1px" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export function BatteryMediumIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export function BatteryLowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...props} fill="currentColor">
|
||||
<path d="M16 20H8V6h8m.67-2H15V2H9v2H7.33C6.6 4 6 4.6 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34c.74 0 1.33-.59 1.33-1.33V5.33C18 4.6 17.4 4 16.67 4M15 16H9v3h6zm0-4.5H9v3h6z" />
|
||||
<path d="M16 17H8V6h8m.7-2H15V2H9v2H7.3A1.3 1.3 0 0 0 6 5.3v15.4q.1 1.2 1.3 1.3h9.4a1.3 1.3 0 0 0 1.3-1.3V5.3q-.1-1.2-1.3-1.3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CopyIcon } from "lucide-react"
|
||||
import { copyToClipboard } from "@/lib/utils"
|
||||
import { Button } from "./button"
|
||||
import { Input } from "./input"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
||||
|
||||
export function InputCopy({ value, id, name }: { value: string; id: string; name: string }) {
|
||||
return (
|
||||
@@ -14,25 +14,23 @@ export function InputCopy({ value, id, name }: { value: string; id: string; name
|
||||
"h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
|
||||
}
|
||||
></div>
|
||||
<TooltipProvider delayDuration={100} disableHoverableContent>
|
||||
<Tooltip disableHoverableContent={true}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"link"}
|
||||
className="absolute end-0 top-0"
|
||||
onClick={() => copyToClipboard(value)}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans>Click to copy</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip disableHoverableContent={true}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"link"}
|
||||
className="absolute end-0 top-0"
|
||||
onClick={() => copyToClipboard(value)}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans>Click to copy</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
function TooltipProvider({ delayDuration = 50, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export function getLocale() {
|
||||
}
|
||||
locale = (locale || "en").split("-")[0]
|
||||
// use en if locale is not in languages
|
||||
if (!languages.some((l) => l.lang === locale)) {
|
||||
if (!languages.some((l) => l[0] === locale)) {
|
||||
locale = "en"
|
||||
}
|
||||
return locale
|
||||
|
||||
@@ -1,147 +1,32 @@
|
||||
export default [
|
||||
{
|
||||
lang: "ar",
|
||||
label: "العربية",
|
||||
e: "🇵🇸",
|
||||
},
|
||||
{
|
||||
lang: "bg",
|
||||
label: "Български",
|
||||
e: "🇧🇬",
|
||||
},
|
||||
{
|
||||
lang: "cs",
|
||||
label: "Čeština",
|
||||
e: "🇨🇿",
|
||||
},
|
||||
{
|
||||
lang: "da",
|
||||
label: "Dansk",
|
||||
e: "🇩🇰",
|
||||
},
|
||||
{
|
||||
lang: "de",
|
||||
label: "Deutsch",
|
||||
e: "🇩🇪",
|
||||
},
|
||||
{
|
||||
lang: "en",
|
||||
label: "English",
|
||||
e: "🇺🇸",
|
||||
},
|
||||
{
|
||||
lang: "es",
|
||||
label: "Español",
|
||||
e: "🇲🇽",
|
||||
},
|
||||
{
|
||||
lang: "fa",
|
||||
label: "فارسی",
|
||||
e: "🇮🇷",
|
||||
},
|
||||
{
|
||||
lang: "fr",
|
||||
label: "Français",
|
||||
e: "🇫🇷",
|
||||
},
|
||||
{
|
||||
lang: "he",
|
||||
label: "עברית",
|
||||
e: "🕎",
|
||||
},
|
||||
{
|
||||
lang: "hr",
|
||||
label: "Hrvatski",
|
||||
e: "🇭🇷",
|
||||
},
|
||||
{
|
||||
lang: "hu",
|
||||
label: "Magyar",
|
||||
e: "🇭🇺",
|
||||
},
|
||||
{
|
||||
lang: "it",
|
||||
label: "Italiano",
|
||||
e: "🇮🇹",
|
||||
},
|
||||
{
|
||||
lang: "ja",
|
||||
label: "日本語",
|
||||
e: "🇯🇵",
|
||||
},
|
||||
{
|
||||
lang: "ko",
|
||||
label: "한국어",
|
||||
e: "🇰🇷",
|
||||
},
|
||||
{
|
||||
lang: "nl",
|
||||
label: "Nederlands",
|
||||
e: "🇳🇱",
|
||||
},
|
||||
{
|
||||
lang: "no",
|
||||
label: "Norsk",
|
||||
e: "🇳🇴",
|
||||
},
|
||||
{
|
||||
lang: "pl",
|
||||
label: "Polski",
|
||||
e: "🇵🇱",
|
||||
},
|
||||
{
|
||||
lang: "pt",
|
||||
label: "Português",
|
||||
e: "🇧🇷",
|
||||
},
|
||||
{
|
||||
lang: "ru",
|
||||
label: "Русский",
|
||||
e: "🇷🇺",
|
||||
},
|
||||
{
|
||||
lang: "sl",
|
||||
label: "Slovenščina",
|
||||
e: "🇸🇮",
|
||||
},
|
||||
{
|
||||
lang: "sr",
|
||||
label: "Српски",
|
||||
e: "🇷🇸",
|
||||
},
|
||||
{
|
||||
lang: "sv",
|
||||
label: "Svenska",
|
||||
e: "🇸🇪",
|
||||
},
|
||||
{
|
||||
lang: "tr",
|
||||
label: "Türkçe",
|
||||
e: "🇹🇷",
|
||||
},
|
||||
{
|
||||
lang: "uk",
|
||||
label: "Українська",
|
||||
e: "🇺🇦",
|
||||
},
|
||||
{
|
||||
lang: "vi",
|
||||
label: "Tiếng Việt",
|
||||
e: "🇻🇳",
|
||||
},
|
||||
{
|
||||
lang: "zh-CN",
|
||||
label: "简体中文",
|
||||
e: "🇨🇳",
|
||||
},
|
||||
{
|
||||
lang: "zh-HK",
|
||||
label: "繁體中文",
|
||||
e: "🇭🇰",
|
||||
},
|
||||
{
|
||||
lang: "zh",
|
||||
label: "繁體中文",
|
||||
e: "🇹🇼",
|
||||
},
|
||||
["ar", "العربية", "🇵🇸"],
|
||||
["bg", "Български", "🇧🇬"],
|
||||
["cs", "Čeština", "🇨🇿"],
|
||||
["da", "Dansk", "🇩🇰"],
|
||||
["de", "Deutsch", "🇩🇪"],
|
||||
["en", "English", "🇬🇧"],
|
||||
["es", "Español", "🇪🇸"],
|
||||
["fa", "فارسی", "🇮🇷"],
|
||||
["fr", "Français", "🇫🇷"],
|
||||
["he", "עברית", "🕎"],
|
||||
["hr", "Hrvatski", "🇭🇷"],
|
||||
["hu", "Magyar", "🇭🇺"],
|
||||
["id", "Indonesia", "🇮🇩"],
|
||||
["it", "Italiano", "🇮🇹"],
|
||||
["ja", "日本語", "🇯🇵"],
|
||||
["ko", "한국어", "🇰🇷"],
|
||||
["nl", "Nederlands", "🇳🇱"],
|
||||
["no", "Norsk", "🇳🇴"],
|
||||
["pl", "Polski", "🇵🇱"],
|
||||
["pt", "Português", "🇵🇹"],
|
||||
["ru", "Русский", "🇷🇺"],
|
||||
["sl", "Slovenščina", "🇸🇮"],
|
||||
["sr", "Српски", "🇷🇸"],
|
||||
["sv", "Svenska", "🇸🇪"],
|
||||
["tr", "Türkçe", "🇹🇷"],
|
||||
["uk", "Українська", "🇺🇦"],
|
||||
["vi", "Tiếng Việt", "🇻🇳"],
|
||||
["zh-CN", "简体中文", "🇨🇳"],
|
||||
["zh-HK", "繁體中文", "🇭🇰"],
|
||||
["zh", "繁體中文", "🇹🇼"],
|
||||
] as const
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
$pausedSystems,
|
||||
$upSystems,
|
||||
} from "@/lib/stores"
|
||||
import { updateFavicon } from "@/lib/utils"
|
||||
import { getVisualStringWidth, updateFavicon } from "@/lib/utils"
|
||||
import type { SystemRecord } from "@/types"
|
||||
import { SystemStatus } from "./enums"
|
||||
|
||||
@@ -79,7 +79,7 @@ function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: System
|
||||
|
||||
// Update longest system name length
|
||||
const longestName = $longestSystemNameLen.get()
|
||||
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, changedSystem?.name.length || 0)
|
||||
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, getVisualStringWidth(changedSystem?.name || ""))
|
||||
if (nameLen > longestName) {
|
||||
$longestSystemNameLen.set(nameLen)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function copyToClipboard(content: string) {
|
||||
duration,
|
||||
description: t`Copied to clipboard`,
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
$copyContent.set(content)
|
||||
}
|
||||
}
|
||||
@@ -316,7 +316,7 @@ export const getHostDisplayValue = (system: SystemRecord): string => system.host
|
||||
export const generateToken = () => {
|
||||
try {
|
||||
return crypto?.randomUUID()
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return Array.from({ length: 2 }, () => (performance.now() * Math.random()).toString(16).replace(".", "-")).join("-")
|
||||
}
|
||||
}
|
||||
@@ -429,6 +429,30 @@ export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
|
||||
}) as T
|
||||
}
|
||||
|
||||
/** Get the visual width of a string, accounting for full-width characters */
|
||||
export function getVisualStringWidth(str: string): number {
|
||||
let width = 0
|
||||
for (const char of str) {
|
||||
const code = char.codePointAt(0) || 0
|
||||
// Hangul Jamo and Syllables are often slightly thinner than Hanzi/Kanji
|
||||
if ((code >= 0x1100 && code <= 0x115f) || (code >= 0xac00 && code <= 0xd7af)) {
|
||||
width += 1.8
|
||||
continue
|
||||
}
|
||||
// Count CJK and other full-width characters as 2 units, others as 1
|
||||
// Arabic and Cyrillic are counted as 1
|
||||
const isFullWidth =
|
||||
(code >= 0x2e80 && code <= 0x9fff) || // CJK Radicals, Symbols, and Ideographs
|
||||
(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
|
||||
(code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms
|
||||
(code >= 0xff00 && code <= 0xff60) || // Fullwidth Forms
|
||||
(code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Symbols
|
||||
code > 0xffff // Emojis and other supplementary plane characters
|
||||
width += isFullWidth ? 2 : 1
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
/** 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))
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ar\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-25 19:15\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"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "تم تحديد {0} من {1} صف"
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# نواة} other {# نواة}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} يوم} other {{countString} أيام}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} ساعة} other {{countString} ساع
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# خيط} other {# خيط}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 ساعة"
|
||||
@@ -182,6 +190,11 @@ msgstr "متوسط"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "متوسط استخدام وحدة المعالجة المركزية للحاويات"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "المتوسط ينخفض أقل من <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "النسخ الاحتياطية"
|
||||
msgid "Bandwidth"
|
||||
msgstr "عرض النطاق الترددي"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "بطارية"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "البطارية"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "أصبح غير نشط"
|
||||
msgid "Before"
|
||||
msgstr "قبل"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "أقل من {0}{1} في آخر {2, plural, one {# دقيقة} other {# دقائق}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "يدعم بيزيل بروتوكول OpenID Connect والعديد من مزوّدي المصادقة عبر بروتوكول OAuth2."
|
||||
@@ -568,7 +594,7 @@ msgstr "التوثيق"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -594,7 +620,7 @@ msgstr "تعديل"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Edit {foo}"
|
||||
msgstr ""
|
||||
msgstr "إضافة {foo}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
@@ -628,6 +654,10 @@ msgstr "أدخل عنوان البريد الإشباكي..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "أدخل كلمة المرور لمرة واحدة الخاصة بك."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "مؤقت"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "سيتم حذف الأنظمة الحالية غير المعرفة في
|
||||
msgid "Exited active"
|
||||
msgstr "خرج نشطًا"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "ينتهي بعد ساعة واحدة أو عند إعادة تشغيل المحور."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "تصدير"
|
||||
@@ -803,11 +837,6 @@ msgstr "غير نشط"
|
||||
msgid "Invalid email address."
|
||||
msgstr "عنوان البريد الإشباكي غير صالح."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "النواة"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "اللغة"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "الحد الأقصى دقيقة"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "متوسط الاستخدام لكل نواة"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "النسبة المئوية للوقت المقضي في كل حالة"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "دائم"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "الاستمرارية"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "يرجى <0>تكوين خادم SMTP</0> لضمان تسليم التنبيهات."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "حفظ الإعدادات"
|
||||
msgid "Save system"
|
||||
msgstr "احفظ النظام"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "محفوظ في قاعدة البيانات ولا ينتهي حتى تقوم بتعطيله."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "جدولة"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "تنسيق الوقت"
|
||||
msgid "To email(s)"
|
||||
msgstr "إلى البريد الإشباكي"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "تبديل الشبكة"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "يتم التفعيل عندما تنخفض شحنة البطارية أقل من عتبة معينة"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "يتم التفعيل عندما يتجاوز الجمع بين الصعود/الهبوط عتبة معينة"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "غير محدود"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "قيد التشغيل"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "يتم التحديث كل 10 دقائق."
|
||||
msgid "Upload"
|
||||
msgstr "رفع"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "مدة التشغيل"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "إشعارات Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "عند التفعيل، يسمح هذا الرمز المميز للوكلاء بالتسجيل الذاتي دون إنشاء نظام مسبق. ينتهي بعد ساعة واحدة أو عند إعادة تشغيل المحور."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "عند التفعيل، يسمح هذا الرمز المميز للوكلاء بالتسجيل الذاتي دون إنشاء نظام مسبق."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: bg\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} от {1} селектирани."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# ядро} other {# ядра}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} ден} other {{countString} дни}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} час} other {{countString} часа
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# нишка} other {# нишки}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 час"
|
||||
@@ -182,6 +190,11 @@ msgstr "Средно"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Средно използване на процесора на контейнерите"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Средната стойност пада под <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Архиви"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Bandwidth на мрежата"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Батерия"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Стана неактивен"
|
||||
msgid "Before"
|
||||
msgstr "Преди"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Под {0}{1} в последните {2, plural, one {# минута} other {# минути}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel поддържа OpenID Connect и много други OAuth2 доставчици за удостоверяване."
|
||||
@@ -568,7 +594,7 @@ msgstr "Документация"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "Въведи имейл адрес..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Въведете Вашата еднократна парола."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Ефимерен"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Съществуващи системи които не са дефин
|
||||
msgid "Exited active"
|
||||
msgstr "Излезе активно"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Изтича след един час или при рестартиране на хъба."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Експортиране"
|
||||
@@ -803,11 +837,6 @@ msgstr "Неактивен"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Невалиден имейл адрес."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Linux Kernel"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Език"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Максимум 1 минута"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Средно използване на ядро"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Процент време, прекарано във всяко състояние"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Постоянен"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Устойчивост"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Моля <0>конфигурурай SMTP сървър</0> за да се подсигуриш, че тревогите са доставени."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Запази настройките"
|
||||
msgid "Save system"
|
||||
msgstr "Запази система"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Запазен е в базата данни и не изтича, докато не го деактивирате."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "График"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Формат на времето"
|
||||
msgid "To email(s)"
|
||||
msgstr "До имейл(ите)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Превключване на мрежа"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Задейства се, когато употребата на паме
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Задейства се, когато някой даден сензор надвиши зададен праг"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Задейства се, когато зарядът на батерията падне под зададен праг"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Задейства се, когато комбинираното качване/сваляне надвиши зададен праг"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Неограничено"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Нагоре"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Актуализира се на всеки 10 минути."
|
||||
msgid "Upload"
|
||||
msgstr "Качване"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Време на работа"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Пуш нотификации"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Когато е активиран, този символ позволява на агентите да се регистрират сами без предварително създаване на система. Изтича след един час или при рестартиране на хъба."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Когато е активиран, този символ позволява на агентите да се регистрират сами без предварително създаване на система."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: cs\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} z {1} vybraných řádků."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# jádro} few {# jádra} many {# jader} other {# jader}}"
|
||||
|
||||
#: 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í}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} Hodina} few {{countString} Hodiny} ma
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# vlákno} few {# vlákna} many {# vláken} other {# vláken}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 hodina"
|
||||
@@ -182,6 +190,11 @@ msgstr "Průměr"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Průměrné využití CPU kontejnerů"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Průměr klesne pod <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Zálohy"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Přenos"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Baterie"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Stal se neaktivním"
|
||||
msgid "Before"
|
||||
msgstr "Před"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Pod {0}{1} za {2, plural, one {poslední # minutu} few {poslední # minuty} other {posledních # minut}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel podporuje OpenID Connect a mnoho poskytovatelů OAuth2 ověřování."
|
||||
@@ -568,7 +594,7 @@ msgstr "Dokumentace"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "Zadejte e-mailovou adresu..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Zadejte Vaše jednorázové heslo."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Efemérní"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Stávající systémy, které nejsou definovány v <0>config.yml</0>, bu
|
||||
msgid "Exited active"
|
||||
msgstr "Ukončeno aktivně"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Vyprší po jedné hodině nebo při restartu hubu."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Exportovat"
|
||||
@@ -803,11 +837,6 @@ msgstr "Neaktivní"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Neplatná e-mailová adresa."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Jádro"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jazyk"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Max. 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Průměrné využití na jádro"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Procento času strávěného v každém stavu"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Trvalý"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Trvalost"
|
||||
|
||||
#: 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."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Uložit nastavení"
|
||||
msgid "Save system"
|
||||
msgstr "Uložit systém"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Uložen v databázi a nevyprší, dokud jej nezablokujete."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Plán"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Formát času"
|
||||
msgid "To email(s)"
|
||||
msgstr "Na email(y)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Přepnout mřížku"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Spustí se, když využití paměti během 5 minut překročí prahovou
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Spustí se, když některý senzor překročí prahovou hodnotu"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Spustí se, když úroveň nabití baterie klesne pod prahovou hodnotu"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Spustí se, když kombinace up/down překročí prahovou hodnotu"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Neomezeno"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Funkční"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Aktualizováno každých 10 minut."
|
||||
msgid "Upload"
|
||||
msgstr "Odeslání"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Doba provozu"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push oznámení"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Pokud je povoleno, tento token umožňuje agentům, aby se sami zaregistrovali bez předchozího vytvoření systému. Vyprší po jedné hodině nebo po restartu uzlu."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Pokud je povoleno, umožňuje tento token agentům samo-registraci bez předchozího vytvoření systému."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: da\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-19 10:55\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} af {1} række(r) valgt."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# kerne} other {# kerner}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} dag} other {{countString} dage}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} time} other {{countString} timer}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# tråd} other {# tråde}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 time"
|
||||
@@ -182,6 +190,11 @@ msgstr "Gennemsnitlig"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Gennemsnitlig CPU udnyttelse af containere"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Gennemsnit falder under <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Sikkerhedskopier"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Båndbredde"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Batteri"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Blev inaktiv"
|
||||
msgid "Before"
|
||||
msgstr "Før"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Under {0}{1} i sidste {2, plural, one {# minut} other {# minutter}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel understøtter OpenID Connect og mange OAuth2 godkendelsesudbydere."
|
||||
@@ -442,7 +468,7 @@ msgstr "CPU Peak"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "CPU time"
|
||||
msgstr ""
|
||||
msgstr "CPU tid"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Time Breakdown"
|
||||
@@ -568,7 +594,7 @@ msgstr "Dokumentation"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "Indtast e-mailadresse..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Indtast din engangsadgangskode."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Efemer"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -648,7 +678,7 @@ msgstr "Overskrider {0}{1} i sidste {2, plural, one {# minut} other {# minutter}
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Exec main PID"
|
||||
msgstr ""
|
||||
msgstr "Exec vigtigste PID"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
|
||||
@@ -658,6 +688,10 @@ msgstr "Eksisterende systemer ikke defineret i <0>config.yml</0> vil blive slett
|
||||
msgid "Exited active"
|
||||
msgstr "Afsluttet aktiv"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Udløber efter en time eller ved hub-genstart."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Eksporter"
|
||||
@@ -720,7 +754,7 @@ msgstr "Fingeraftryk"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inaktiv"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ugyldig email adresse."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kerne"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprog"
|
||||
@@ -827,7 +856,7 @@ msgstr "Livscyklus"
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "limit"
|
||||
msgstr ""
|
||||
msgstr "grænse"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
@@ -883,7 +912,7 @@ msgstr "Leder du i stedet for efter hvor du kan oprette alarmer? Klik på klokke
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Main PID"
|
||||
msgstr ""
|
||||
msgstr "Primær PID"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Manage display and notification preferences."
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Maks. 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -913,7 +943,7 @@ msgstr "Hukommelsesgrænse"
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Memory Peak"
|
||||
msgstr ""
|
||||
msgstr "Hukommelsesspids"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -939,7 +969,7 @@ msgstr "Navn"
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr ""
|
||||
msgstr "Net"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Gennemsnitlig udnyttelse pr. kerne"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Procentdel af tid brugt i hver tilstand"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Vedholdenhed"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Konfigurer <0>en SMTP server</0> for at sikre at alarmer bliver leveret."
|
||||
@@ -1208,7 +1246,7 @@ msgstr "Genoptag"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgctxt "Root disk label"
|
||||
msgid "Root"
|
||||
msgstr ""
|
||||
msgstr "Root"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Gem indstillinger"
|
||||
msgid "Save system"
|
||||
msgstr "Gem system"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Gemt i databasen og udløber ikke, før du deaktiverer det."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Planlæg"
|
||||
@@ -1366,7 +1408,7 @@ msgstr "Gennemsnitlig system belastning over tid"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Systemd Services"
|
||||
msgstr ""
|
||||
msgstr "Systemd Services"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Tidsformat"
|
||||
msgid "To email(s)"
|
||||
msgstr "Til email(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Slå gitter til/fra"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Udløser når 5 minut belastning gennemsnit overstiger en tærskel"
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Udløser når en sensor overstiger en tærskel"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Udløses når batteriniveauet falder under en tærskel"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Udløses når de kombinerede op/ned overstiger en tærskel"
|
||||
@@ -1541,7 +1587,7 @@ msgstr "Type"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Unit file"
|
||||
msgstr ""
|
||||
msgstr "Enhed fil"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -1561,10 +1607,10 @@ msgstr "Ukendt"
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Unlimited"
|
||||
msgstr ""
|
||||
msgstr "Ubegrænset"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Oppe"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Opdateret hver 10. minut."
|
||||
msgid "Upload"
|
||||
msgstr "Overfør"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Oppetid"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push notifikationer"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Når aktiveret tillader denne nøgle agenter at selvregistrere uden forudgående systemoprettelse. Udløber efter en time eller ved hub-genstart."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Når aktiveret, tillader denne token agenter at registrere sig selv uden forudgående systemoprettelse."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} von {1} Zeile(n) ausgewählt."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# Kern} other {# Kerne}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} Tag} other {{countString} Tage}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} Stunde} other {{countString} Stunden}
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# Thread} other {# Threads}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 Stunde"
|
||||
@@ -182,6 +190,11 @@ msgstr "Durchschnitt"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Durchschnittliche CPU-Auslastung der Container"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Durchschnitt unterschreitet <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Backups"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Bandbreite"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Batterie"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Wurde inaktiv"
|
||||
msgid "Before"
|
||||
msgstr "Vor"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Unterschreitet {0}{1} in den letzten {2, plural, one {# Minute} other {# Minuten}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter."
|
||||
@@ -568,7 +594,7 @@ msgstr "Dokumentation"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "E-Mail-Adresse eingeben..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Geben Sie Ihr Einmalpasswort ein."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Flüchtig"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Bestehende Systeme, die nicht in der <0>config.yml</0> definiert sind, w
|
||||
msgid "Exited active"
|
||||
msgstr "Beendet aktiv"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Läuft nach einer Stunde oder bei Hub-Neustart ab."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Exportieren"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inaktiv"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ungültige E-Mail-Adresse."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Max 1 Min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Durchschnittliche Auslastung pro Kern"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Prozentsatz der Zeit in jedem Zustand"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Persistenz"
|
||||
|
||||
#: 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."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Einstellungen speichern"
|
||||
msgid "Save system"
|
||||
msgstr "System speichern"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "In der Datenbank gespeichert und läuft nicht ab, bis Sie es deaktivieren."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Zeitplan"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Zeitformat"
|
||||
msgid "To email(s)"
|
||||
msgstr "An E-Mail(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Raster umschalten"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwell
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Löst aus, wenn der Batterieladestand unter einen Schwellenwert fällt"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Löst aus, wenn die kombinierte Up- und Downloadrate einen Schwellenwert überschreitet"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Unbegrenzt"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "aktiv"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Alle 10 Minuten aktualisiert."
|
||||
msgid "Upload"
|
||||
msgstr "Hochladen"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Betriebszeit"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push-Benachrichtigungen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Wenn aktiviert, ermöglicht dieser Token Agents, sich selbst zu registrieren, ohne vorherige Systemerstellung. Läuft nach einer Stunde oder beim Hub-Neustart ab."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Wenn aktiviert, ermöglicht dieser Token Agenten die Selbstregistrierung ohne vorherige Systemerstellung."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -19,6 +19,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} of {1} row(s) selected."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# core} other {# cores}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
@@ -31,6 +35,10 @@ msgstr "{count, plural, one {{countString} hour} other {{countString} hours}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# thread} other {# threads}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 hour"
|
||||
@@ -177,6 +185,11 @@ msgstr "Average"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Average CPU utilization of containers"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Average drops below <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -209,7 +222,13 @@ msgstr "Backups"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Bandwidth"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Battery"
|
||||
|
||||
@@ -225,6 +244,13 @@ msgstr "Became inactive"
|
||||
msgid "Before"
|
||||
msgstr "Before"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
@@ -563,7 +589,7 @@ msgstr "Documentation"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -623,6 +649,10 @@ msgstr "Enter email address..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Enter your one-time password."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Ephemeral"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -653,6 +683,10 @@ msgstr "Existing systems not defined in <0>config.yml</0> will be deleted. Pleas
|
||||
msgid "Exited active"
|
||||
msgstr "Exited active"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Expires after one hour or on hub restart."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
@@ -798,11 +832,6 @@ msgstr "Inactive"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Invalid email address."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Language"
|
||||
@@ -895,6 +924,7 @@ msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1082,6 +1112,14 @@ msgstr "Per-core average utilization"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Percentage of time spent in each state"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Persistence"
|
||||
|
||||
#: 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."
|
||||
@@ -1238,6 +1276,10 @@ msgstr "Save Settings"
|
||||
msgid "Save system"
|
||||
msgstr "Save system"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Saved in the database and does not expire until you disable it."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Schedule"
|
||||
@@ -1434,8 +1476,8 @@ msgstr "Time format"
|
||||
msgid "To email(s)"
|
||||
msgstr "To email(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Toggle grid"
|
||||
|
||||
@@ -1504,6 +1546,10 @@ msgstr "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Triggers when any sensor exceeds a threshold"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Triggers when battery charge drops below a threshold"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Triggers when combined up/down exceeds a threshold"
|
||||
@@ -1559,7 +1605,7 @@ msgid "Unlimited"
|
||||
msgstr "Unlimited"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Up"
|
||||
@@ -1586,7 +1632,7 @@ msgstr "Updated every 10 minutes."
|
||||
msgid "Upload"
|
||||
msgstr "Upload"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Uptime"
|
||||
|
||||
@@ -1658,8 +1704,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push notifications"
|
||||
|
||||
#: 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 "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "When enabled, this token allows agents to self-register without prior system creation."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-01 23:32\n"
|
||||
"PO-Revision-Date: 2025-12-14 09:39\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} de {1} fila(s) seleccionada(s)."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# núcleo} other {# núcleos}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} día} other {{countString} días}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} hora} other {{countString} horas}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# hilo} other {# hilos}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 hora"
|
||||
@@ -182,6 +190,11 @@ msgstr "Promedio"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Utilización promedio de CPU de los contenedores"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "El promedio cae por debajo de <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Copias de seguridad"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Ancho de banda"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Batería"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Se desactivó"
|
||||
msgid "Before"
|
||||
msgstr "Antes"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Por debajo de {0}{1} en el último {2, plural, one {# minuto} other {# minutos}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel admite OpenID Connect y muchos proveedores de autenticación OAuth2."
|
||||
@@ -331,7 +357,7 @@ msgstr "Verifica tu servicio de notificaciones"
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
msgstr "Limpiar"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
@@ -568,7 +594,7 @@ msgstr "Documentación"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "Ingresa dirección de correo..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Ingrese su contraseña de un solo uso."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Efímero"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Los sistemas existentes no definidos en <0>config.yml</0> serán elimina
|
||||
msgid "Exited active"
|
||||
msgstr "Salió activo"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Expira después de una hora o al reiniciar el hub."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Exportar"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inactivo"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Dirección de correo electrónico no válida."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Máx. 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Uso promedio por núcleo"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Porcentaje de tiempo dedicado a cada estado"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanente"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Persistencia"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Por favor, <0>configura un servidor SMTP</0> para asegurar que las alertas sean entregadas."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Guardar configuración"
|
||||
msgid "Save system"
|
||||
msgstr "Guardar sistema"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Guardado en la base de datos y no expira hasta que lo desactives."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Programar"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Formato de hora"
|
||||
msgid "To email(s)"
|
||||
msgstr "A correo(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Alternar cuadrícula"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Se activa cuando la carga media de 5 minutos supera un umbral"
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Se activa cuando cualquier sensor supera un umbral"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Se activa cuando la carga de la batería baja de un umbral"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Se activa cuando la suma de subida/bajada supera un umbral"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Ilimitado"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Activo"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Actualizado cada 10 minutos."
|
||||
msgid "Upload"
|
||||
msgstr "Cargar"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Tiempo de actividad"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Notificaciones Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Cuando está habilitado, este token permite que los agentes se auto-registren sin crear previamente el sistema. Expira después de una hora o al reiniciar el hub."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Cuando está habilitado, este token permite a los agentes registrarse automáticamente sin creación previa del sistema."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fa\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Persian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} از {1} ردیف انتخاب شده است."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# هسته} other {# هسته}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} روز} other {{countString} روز}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} ساعت} other {{countString} ساع
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# رشته} other {# رشته}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "۱ ساعت"
|
||||
@@ -182,6 +190,11 @@ msgstr "میانگین"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "میانگین استفاده از CPU کانتینرها"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "میانگین به زیر <0>{value}{0}</0> میافتد"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "پشتیبانگیریها"
|
||||
msgid "Bandwidth"
|
||||
msgstr "پهنای باند"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "باتری"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "باتری"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "غیرفعال شد"
|
||||
msgid "Before"
|
||||
msgstr "قبل از"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "زیر {0}{1} در آخرین {2, plural, one {# دقیقه} other {# دقیقه}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "بِزل از OpenID Connect و بسیاری از ارائهدهندگان احراز هویت OAuth2 پشتیبانی میکند."
|
||||
@@ -331,7 +357,7 @@ msgstr "سرویس اطلاعرسانی خود را بررسی کنید"
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
msgstr "پاک کردن"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
@@ -568,7 +594,7 @@ msgstr "مستندات"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "آدرس ایمیل را وارد کنید..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "رمز عبور یکبار مصرف خود را وارد کنید."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "گذرا"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -648,7 +678,7 @@ msgstr "در {2, plural, one {# دقیقه} other {# دقیقه}} گذشته ا
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Exec main PID"
|
||||
msgstr ""
|
||||
msgstr "PID اصلی اجرایی"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
|
||||
@@ -658,6 +688,10 @@ msgstr "سیستمهای موجود که در <0>config.yml</0> تعریف ن
|
||||
msgid "Exited active"
|
||||
msgstr "خروج فعال"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "پس از یک ساعت یا راهاندازی مجدد هاب منقضی میشود."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "خروجی گرفتن"
|
||||
@@ -803,11 +837,6 @@ msgstr "غیرفعال"
|
||||
msgid "Invalid email address."
|
||||
msgstr "آدرس ایمیل نامعتبر است."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "هسته"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "زبان"
|
||||
@@ -827,7 +856,7 @@ msgstr "چرخه حیات"
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "limit"
|
||||
msgstr ""
|
||||
msgstr "محدودیت"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
@@ -883,7 +912,7 @@ msgstr "به دنبال جایی برای ایجاد هشدار هستید؟ ر
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Main PID"
|
||||
msgstr ""
|
||||
msgstr "PID اصلی"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Manage display and notification preferences."
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "حداکثر ۱ دقیقه"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "میانگین استفاده در هر هسته"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "درصد زمان صرف شده در هر حالت"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "دائمی"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "ماندگاری"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "لطفاً برای اطمینان از تحویل هشدارها، یک <0>سرور SMTP پیکربندی کنید</0>."
|
||||
@@ -1208,7 +1246,7 @@ msgstr "ادامه"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgctxt "Root disk label"
|
||||
msgid "Root"
|
||||
msgstr ""
|
||||
msgstr "ریشه"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
@@ -1243,6 +1281,10 @@ msgstr "ذخیره تنظیمات"
|
||||
msgid "Save system"
|
||||
msgstr "ذخیره سیستم"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "در پایگاه داده ذخیره شده و تا زمانی که آن را غیرفعال نکنید، منقضی نمیشود."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "برنامهریزی"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "فرمت زمان"
|
||||
msgid "To email(s)"
|
||||
msgstr "به ایمیل(ها)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "تغییر نمایش جدول"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "هنگامی که میانگین بار ۵ دقیقهای از یک
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "هنگامی که هر حسگری از یک آستانه فراتر رود، فعال میشود"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "زمانی که شارژ باتری زیر آستانه قرار میگیرد، فعال میشود"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "هنگامی که مجموع بالا/پایین از یک آستانه فراتر رود، فعال میشود"
|
||||
@@ -1541,7 +1587,7 @@ msgstr "نوع"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Unit file"
|
||||
msgstr ""
|
||||
msgstr "فایل واحد"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "نامحدود"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "فعال"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "هر ۱۰ دقیقه بهروزرسانی میشود."
|
||||
msgid "Upload"
|
||||
msgstr "آپلود"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "آپتایم"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "اعلانهای Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "هنگامی که فعال است، این توکن به عاملها اجازه خودثبتنامی بدون ایجاد سیستم قبلی میدهد. پس از یک ساعت یا در راهاندازی مجدد هاب منقضی میشود."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "هنگامی که فعال باشد، این توکن به عوامل اجازه میدهد بدون ایجاد سیستم قبلی، خود را ثبت کنند."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2026-01-09 21:08\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} sur {1} ligne(s) sélectionnée(s)."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# cœur} other {# cœurs}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} jour} other {{countString} jours}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} heure} other {{countString} heures}}"
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} minute} other {{countString} minutes}}"
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# fil} other {# fils}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 heure"
|
||||
@@ -182,6 +190,11 @@ msgstr "Moyenne"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Utilisation moyenne du CPU des conteneurs"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "La moyenne descend en dessous de <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Sauvegardes"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Bande passante"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Batterie"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Devenu inactif"
|
||||
msgid "Before"
|
||||
msgstr "Avant"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Inférieur à {0}{1} dans {2, plural, one {la dernière # minute} other {les dernières # minutes}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel prend en charge OpenID Connect et de nombreux fournisseurs d'authentification OAuth2."
|
||||
@@ -512,7 +538,7 @@ msgstr "Supprimer l'empreinte"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
msgstr "Description"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
@@ -568,11 +594,11 @@ msgstr "Documentation"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
msgstr "Injoignable"
|
||||
msgstr "Hors ligne"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
@@ -628,6 +654,10 @@ msgstr "Entrez l'adresse email..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Entrez votre mot de passe à usage unique."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Éphémère"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Les systèmes existants non définis dans <0>config.yml</0> seront suppr
|
||||
msgid "Exited active"
|
||||
msgstr "Sorti actif"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Expire après une heure ou au redémarrage du hub."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Exporter"
|
||||
@@ -724,7 +758,7 @@ msgstr "Micrologiciel"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Pour <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Pendant <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Forgot password?"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inactif"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Adresse email invalide."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Noyau"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -939,7 +969,7 @@ msgstr "Nom"
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
msgstr "Rés"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Utilisation moyenne par cœur"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Pourcentage de temps passé dans chaque état"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Persistance"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Veuillez <0>configurer un serveur SMTP</0> pour garantir la livraison des alertes."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Enregistrer les paramètres"
|
||||
msgid "Save system"
|
||||
msgstr "Sauvegarder le système"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Enregistré dans la base de données et n'expire pas tant que vous ne le désactivez pas."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Programmer"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Format d'heure"
|
||||
msgid "To email(s)"
|
||||
msgstr "Aux email(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Basculer la grille"
|
||||
|
||||
@@ -1499,16 +1541,20 @@ 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 "Se déclenche lorsque la charge moyenne sur 15 minute dépasse un seuil"
|
||||
msgstr "Se déclenche lorsque la charge moyenne sur 15 minutes dépasse un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr "Se déclenche lorsque la charge moyenne sur 5 minute dépasse un seuil"
|
||||
msgstr "Se déclenche lorsque la charge moyenne sur 5 minutes dépasse un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Déclenchement lorsque tout capteur dépasse un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Déclenchement lorsque la charge de la batterie descend en dessous d'un seuil"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Déclenchement lorsque le montant/descendant combinée dépasse un seuil"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Illimité"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Joignable"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Mis à jour toutes les 10 minutes."
|
||||
msgid "Upload"
|
||||
msgstr "Téléverser"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Temps de fonctionnement"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Notifications Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Lorsqu'il est activé, ce token permet aux agents de s'auto-enregistrer sans création préalable du système. Expire après une heure ou au redémarrage du hub."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Lorsqu'il est activé, ce jeton permet aux agents de s'enregistrer automatiquement sans création préalable du système."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: he\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hebrew\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} מתוך {1} שורה(ות) נבחרו."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# ליבה} other {# ליבות}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} יום} two {{countString} ימים} other {{countString} ימים}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} שעה} two {{countString} שעות}
|
||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||
msgstr "{count, plural, one {{countString} דקה} two {{countString} דקות} other {{countString} דקות}}"
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# תהליכון} other {# תהליכונים}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "שעה"
|
||||
@@ -182,6 +190,11 @@ msgstr "ממוצע"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "ניצול ממוצע של CPU בקונטיינרים"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "הממוצע יורד מתחת ל-<0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "גיבויים"
|
||||
msgid "Bandwidth"
|
||||
msgstr "רוחב פס"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "סוללה"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "סוללה"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "הפך ללא פעיל"
|
||||
msgid "Before"
|
||||
msgstr "לפני"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "מתחת ל-{0}{1} ב-{2, plural, one {דקה האחרונה} other {-# הדקות האחרונות}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel תומך ב-OpenID Connect ובספקי אימות רבים של OAuth2."
|
||||
@@ -568,7 +594,7 @@ msgstr "תיעוד"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "הכנס כתובת אימייל..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "הכנס את הסיסמה החד-פעמית שלך."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "זמני"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "מערכות קיימות שלא מוגדרות ב-<0>config.yml</0> י
|
||||
msgid "Exited active"
|
||||
msgstr "יצא פעיל"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "פג תוקף לאחר שעה או בהפעלה מחדש של ה-hub."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "ייצא"
|
||||
@@ -803,11 +837,6 @@ msgstr "לא פעיל"
|
||||
msgid "Invalid email address."
|
||||
msgstr "כתובת אימייל לא תקינה."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "קרנל"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "שפה"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "מקס 1 דק'"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "ניצול ממוצע לליבה"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "אחוז הזמן המוקדש לכל מצב"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "קבוע"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "עקביות"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "אנא <0>הגדר שרת SMTP</0> כדי להבטיח שהתראות יישלחו."
|
||||
@@ -1208,7 +1246,7 @@ msgstr "המשך"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgctxt "Root disk label"
|
||||
msgid "Root"
|
||||
msgstr ""
|
||||
msgstr "שורש"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
@@ -1243,6 +1281,10 @@ msgstr "שמור הגדרות"
|
||||
msgid "Save system"
|
||||
msgstr "שמור מערכת"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "נשמר במסד הנתונים ולא פג תוקף עד שתבטל אותו."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "לוח זמנים"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "פורמט זמן"
|
||||
msgid "To email(s)"
|
||||
msgstr "לאימייל(ים)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "החלף רשת"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "מופעל כאשר ממוצע העומס ל-5 דקות עולה על ס
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "מופעל כאשר כל חיישן עולה על סף"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "מופעל כאשר טעינת הסוללה יורדת מתחת לסף"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "מופעל כאשר השילוב של למעלה/למטה עולה על סף"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "ללא הגבלה"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "למעלה"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "מתעדכן כל 10 דקות."
|
||||
msgid "Upload"
|
||||
msgstr "העלאה"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "זמן פעילות"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / התראות דחיפה"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "כאשר מופעל, token זה מאפשר לסוכנים להירשם עצמאית ללא יצירת מערכת מוקדמת. פג לאחר שעה אחת או בהפעלה מחדש של hub."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "כאשר מופעל, אסימון זה מאפשר לסוכנים להירשם באופן עצמי ללא יצירת מערכת מוקדמת."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\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"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} od {1} redaka izabrano."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# jezgra} few {# jezgre} other {# jezgri}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} dan} other {{countString} dani}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} sat} other {{countString} sati}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# nit} few {# niti} other {# niti}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 sat"
|
||||
@@ -101,7 +109,7 @@ msgstr "Aktivno stanje"
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Add {foo}"
|
||||
msgstr ""
|
||||
msgstr "Dodaj {foo}"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
@@ -182,6 +190,11 @@ msgstr "Prosjek"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Prosječna iskorištenost procesora u spremnicima"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Prosjek pada ispod <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Sigurnosne kopije"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Propusnost"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Baterija"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Postalo neaktivno"
|
||||
msgid "Before"
|
||||
msgstr "Prije"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Ispod {0}{1} u posljednjih {2, plural, one {# minuti} few {# minute} other {# minuta}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel podržava OpenID Connect i mnoge druge OAuth2 davatalje autentifikacije."
|
||||
@@ -280,7 +306,7 @@ msgstr "Otkaži"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Capabilities"
|
||||
msgstr ""
|
||||
msgstr "Mogućnosti"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Capacity"
|
||||
@@ -568,7 +594,7 @@ msgstr "Dokumentacija"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -594,7 +620,7 @@ msgstr "Uredi"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Edit {foo}"
|
||||
msgstr ""
|
||||
msgstr "Uredi {foo}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
@@ -628,6 +654,10 @@ msgstr "Unesite email adresu..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Unesite Vašu jednokratnu lozinku."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Efemeran"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Postojeći sistemi koji nisu definirani u <0>config.yml</0> će biti izb
|
||||
msgid "Exited active"
|
||||
msgstr "Izašlo aktivno"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Istječe nakon jednog sata ili ponovnog pokretanja huba."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Izvezi"
|
||||
@@ -720,7 +754,7 @@ msgstr "Otisak prsta"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
msgstr "Firmver"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -803,11 +837,6 @@ msgstr "Neaktivno"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Nevažeća adresa e-pošte."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Jezgra"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jezik"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Maksimalno 1 minuta"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -926,7 +956,7 @@ msgstr "Upotreba memorije Docker spremnika"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
msgstr "Model"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Prosječna iskorištenost po jezgri"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Postotak vremena provedenog u svakom stanju"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Trajan"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Postojanost"
|
||||
|
||||
#: 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."
|
||||
@@ -1208,7 +1246,7 @@ msgstr "Nastavi"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgctxt "Root disk label"
|
||||
msgid "Root"
|
||||
msgstr ""
|
||||
msgstr "Korijen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Spremi Postavke"
|
||||
msgid "Save system"
|
||||
msgstr "Spremi sustav"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Spremljeno u bazi podataka i ne istječe dok ga ne onemogućite."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Raspored"
|
||||
@@ -1269,7 +1311,7 @@ msgstr "Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način prim
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Select {foo}"
|
||||
msgstr ""
|
||||
msgstr "Odaberi {foo}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Sent"
|
||||
@@ -1366,7 +1408,7 @@ msgstr "Prosječno opterećenje sustava kroz vrijeme"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Systemd Services"
|
||||
msgstr ""
|
||||
msgstr "Systemd servisi"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Format vremena"
|
||||
msgid "To email(s)"
|
||||
msgstr "Primaoci e-pošte"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Uključi/isključi rešetku"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Pokreće se kada prosječna opterećenost sustava unutar 5 minuta prije
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Pokreće se kada bilo koji senzor prijeđe prag"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Pokreće se kada razina baterije padne ispod praga"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Pokreće se kada kombinacija gore/dolje premaši prag"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Neograničeno"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Sustav je podignut"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Ažurirano svakih 10 minuta."
|
||||
msgid "Upload"
|
||||
msgstr "Otpremi"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Vrijeme rada"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push obavijest"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "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."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Kada je omogućen, ovaj token omogućuje agentima da se sami registriraju bez prethodnog stvaranja sustava."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hu\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} a(z) {1} sorból kiválasztva."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# mag} other {# mag}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} nap} other {{countString} nap}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} óra} other {{countString} óra}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# szál} other {# szál}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 óra"
|
||||
@@ -101,7 +109,7 @@ msgstr "Aktív állapot"
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Add {foo}"
|
||||
msgstr ""
|
||||
msgstr "Hozzáadás {foo}"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
@@ -182,6 +190,11 @@ msgstr "Átlag"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Konténerek átlagos CPU kihasználtsága"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Az átlag esik <0>{value}{0}</0> alá"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Biztonsági mentések"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Sávszélesség"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Akk"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Akkumulátor"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Inaktívvá vált"
|
||||
msgid "Before"
|
||||
msgstr "Előtte"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "{0}{1} alatt az elmúlt {2, plural, one {# percben} other {# percben}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "A Beszel támogatja az OpenID Connect-et és számos OAuth2 hitelesítési szolgáltatót."
|
||||
@@ -568,7 +594,7 @@ msgstr "Dokumentáció"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -594,7 +620,7 @@ msgstr "Szerkesztés"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Edit {foo}"
|
||||
msgstr ""
|
||||
msgstr "Szerkesztés {foo}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
@@ -628,6 +654,10 @@ msgstr "Adja meg az e-mail címet..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Adja meg az egyszeri jelszavát."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Átmeneti"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -648,7 +678,7 @@ msgstr "Túllépi a {0}{1} értéket az elmúlt {2, plural, one {# percben} othe
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Exec main PID"
|
||||
msgstr ""
|
||||
msgstr "Fő folyamat PID"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
|
||||
@@ -658,6 +688,10 @@ msgstr "A <0>config.yml</0> fájlban nem definiált meglévő rendszerek törlé
|
||||
msgid "Exited active"
|
||||
msgstr "Aktívként kilépett"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Lejár egy óra után vagy a hub újraindításakor."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Exportálás"
|
||||
@@ -720,7 +754,7 @@ msgstr "Ujjlenyomat"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inaktív"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Érvénytelen e-mail cím."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Nyelv"
|
||||
@@ -883,7 +912,7 @@ msgstr "Inkább azt keresi, hogy hol hozhat létre riasztásokat? Kattintson a c
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Main PID"
|
||||
msgstr ""
|
||||
msgstr "Fő PID"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Manage display and notification preferences."
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Maximum 1 perc"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Átlagos kihasználtság magonként"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Az idő százalékos aránya minden állapotban"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Állandó"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Kitartás"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Kérjük, <0>konfigurálj egy SMTP szervert</0> az értesítések kézbesítésének biztosítása érdekében."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Beállítások mentése"
|
||||
msgid "Save system"
|
||||
msgstr "Rendszer mentése"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Elmentve az adatbázisban és nem jár le, amíg ki nem kapcsolod."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Ütemezés"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Időformátum"
|
||||
msgid "To email(s)"
|
||||
msgstr "E-mailben"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Rács ki- és bekapcsolása"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Riaszt, ha az 5 perces terhelési átlag túllép egy küszöbértéket"
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Korlátlan"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Online"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "10 percenként frissítve."
|
||||
msgid "Upload"
|
||||
msgstr "Feltöltés"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Üzemidő"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push értesítések"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Ha engedélyezve van, ez a token lehetővé teszi az ügynökök önregisztrációját előzetes rendszerlétrehozás nélkül. Egy óra után vagy a hub újraindításakor lejár."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Ha engedélyezve van, ez a token lehetővé teszi az ügynökök számára az önregisztrációt rendszer előzetes létrehozása nélkül."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-20 16:58\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} di {1} righe selezionate."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# core} other {# core}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} giorno} other {{countString} giorni}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} ora} other {{countString} ore}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# thread} other {# thread}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 ora"
|
||||
@@ -182,6 +190,11 @@ msgstr "Media"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Utilizzo medio della CPU dei container"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "La media scende sotto <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Backup"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Larghezza di banda"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Batteria"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Diventato inattivo"
|
||||
msgid "Before"
|
||||
msgstr "Prima"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Sotto {0}{1} negli ultimi {2, plural, one {# minuto} other {# minuti}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel supporta OpenID Connect e molti provider di autenticazione OAuth2."
|
||||
@@ -568,7 +594,7 @@ msgstr "Documentazione"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "Inserisci l'indirizzo email..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Inserisci la tua password monouso."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Effimero"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "I sistemi esistenti non definiti in <0>config.yml</0> verranno eliminati
|
||||
msgid "Exited active"
|
||||
msgstr "Uscito attivo"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Scade dopo un'ora o al riavvio dell'hub."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Esporta"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inattivo"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Indirizzo email non valido."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Lingua"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Utilizzo medio per core"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Percentuale di tempo trascorso in ogni stato"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanente"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Persistenza"
|
||||
|
||||
#: 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."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Salva Impostazioni"
|
||||
msgid "Save system"
|
||||
msgstr "Salva sistema"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Salvato nel database e non scade finché non lo disabiliti."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Pianifica"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Formato orario"
|
||||
msgid "To email(s)"
|
||||
msgstr "A email(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Attiva/disattiva griglia"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Si attiva quando la media di carico di 5 minuti supera una soglia"
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Attiva quando un sensore supera una soglia"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Attiva quando la carica della batteria scende sotto una soglia"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Attiva quando il combinato up/down supera una soglia"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Illimitato"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Attivo"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Aggiornato ogni 10 minuti."
|
||||
msgid "Upload"
|
||||
msgstr "Carica"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Tempo di attività"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Notifiche Webhook / Push"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Quando abilitato, questo token consente agli agenti di auto-registrarsi senza creazione preventiva del sistema. Scade dopo un'ora o al riavvio dell'hub."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Quando abilitato, questo token consente agli agenti di registrarsi automaticamente senza creazione preventiva del sistema."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{1}行のうち{0}行が選択されました。"
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# コア} other {# コア}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} 日} other {{countString} 日}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} 時間} other {{countString} 時間}}
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# スレッド} other {# スレッド}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1時間"
|
||||
@@ -182,6 +190,11 @@ msgstr "平均"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "コンテナの平均CPU使用率"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "平均が<0>{value}{0}</0>を下回っています"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "バックアップ"
|
||||
msgid "Bandwidth"
|
||||
msgstr "帯域幅"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "バッテリー"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "バッテリー"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "非アクティブになった"
|
||||
msgid "Before"
|
||||
msgstr "前"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "過去{2, plural, one {# 分} other {# 分}}で{0}{1}を下回っています"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "BeszelはOpenID Connectと多くのOAuth2認証プロバイダーをサポートしています。"
|
||||
@@ -568,7 +594,7 @@ msgstr "ドキュメント"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "メールアドレスを入力..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "ワンタイムパスワードを入力してください。"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "一時的"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "<0>config.yml</0>に定義されていない既存のシステムは削
|
||||
msgid "Exited active"
|
||||
msgstr "アクティブ状態で終了"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "1時間後、またはハブの再起動時に有効期限が切れます。"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "エクスポート"
|
||||
@@ -803,11 +837,6 @@ msgstr "非アクティブ"
|
||||
msgid "Invalid email address."
|
||||
msgstr "無効なメールアドレスです。"
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "カーネル"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "言語"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "最大1分"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "コアごとの平均使用率"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "各状態で費やした時間の割合"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "永久"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "永続性"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "アラートが配信されるように<0>SMTPサーバーを設定</0>してください。"
|
||||
@@ -1243,6 +1281,10 @@ msgstr "設定を保存"
|
||||
msgid "Save system"
|
||||
msgstr "システムを保存"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "データベースに保存され、無効にするまで有効期限が切れません。"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "スケジュール"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "時間形式"
|
||||
msgid "To email(s)"
|
||||
msgstr "宛先メールアドレス"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "グリッドを切り替え"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "5分間の負荷平均がしきい値を超えたときにトリガー
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "センサーがしきい値を超えたときにトリガーされます"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "バッテリーの充電量がしきい値を下回ったときにトリガーされます"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "上り/下りの合計がしきい値を超えたときにトリガーされます"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "無制限"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "正常"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "10分ごとに更新されます。"
|
||||
msgid "Upload"
|
||||
msgstr "アップロード"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "稼働時間"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / プッシュ通知"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "有効にすると、このトークンはエージェントが事前のシステム作成なしに自己登録することを可能にします。1時間後またはハブの再起動時に期限切れになります。"
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "有効にすると、このトークンによりエージェントは事前のシステム作成なしで自己登録できます。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ko\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Korean\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{1}개의 행 중 {0}개가 선택되었습니다."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# 코어} other {# 코어}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} 일} other {{countString} 일}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} 시간} other {{countString} 시간}}
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# 스레드} other {# 스레드}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1시간"
|
||||
@@ -182,6 +190,11 @@ msgstr "평균"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Docker 컨테이너의 평균 CPU 사용량"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "평균이 <0>{value}{0}</0> 아래로 떨어집니다"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "백업"
|
||||
msgid "Bandwidth"
|
||||
msgstr "대역폭"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "배터리"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "배터리"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "비활성화됨"
|
||||
msgid "Before"
|
||||
msgstr "이전"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "마지막 {2, plural, one {# 분} other {# 분}} 동안 {0}{1} 미만"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel은 OpenID Connect 및 많은 OAuth2 인증 제공자를 지원합니다."
|
||||
@@ -568,7 +594,7 @@ msgstr "문서"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "이메일 주소 입력..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "OTP를 입력하세요."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "일시적"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "<0>config.yml</0>에 정의되지 않은 기존 시스템은 삭제됩
|
||||
msgid "Exited active"
|
||||
msgstr "활성 종료됨"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "한 시간 후 또는 허브 재시작 시 만료됩니다."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "내보내기"
|
||||
@@ -803,11 +837,6 @@ msgstr "비활성"
|
||||
msgid "Invalid email address."
|
||||
msgstr "잘못된 이메일 주소입니다."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr "커널"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "언어"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr "1분간 최댓값"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -1087,6 +1117,14 @@ msgstr "코어별 평균 사용률"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "각 상태에서 보낸 시간의 백분율"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "영구적"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "지속성"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "알림이 전달되도록 <0>SMTP 서버를 구성</0>하세요."
|
||||
@@ -1243,6 +1281,10 @@ msgstr "설정 저장"
|
||||
msgid "Save system"
|
||||
msgstr "시스템 저장"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "데이터베이스에 저장되며 비활성화할 때까지 만료되지 않습니다."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "일정"
|
||||
@@ -1439,8 +1481,8 @@ msgstr "시간 형식"
|
||||
msgid "To email(s)"
|
||||
msgstr "받는사람(들)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "그리드 전환"
|
||||
|
||||
@@ -1509,6 +1551,10 @@ msgstr "5분 부하 평균이 임계값을 초과하면 트리거됩니다."
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "센서가 임계값을 초과할 때 트리거됩니다."
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "배터리 충전량이 임계값 아래로 떨어질 때 트리거됩니다."
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "업로드와 다운로드 대역폭의 합이 임계값을 초과할 때 트리거됩니다."
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "무제한"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "온라인"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "10분마다 업데이트됩니다."
|
||||
msgid "Upload"
|
||||
msgstr "업로드"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "가동 시간"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / 푸시 알림"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "활성화하면 이 토큰을 통해 에이전트가 사전 시스템 생성 없이 자체 등록할 수 있습니다. 1시간 후 또는 허브 재시작 시 만료됩니다."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "활성화되면 이 토큰은 사전 시스템 생성 없이 에이전트가 자체 등록할 수 있도록 합니다."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: nl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-14 22:51\n"
|
||||
"PO-Revision-Date: 2025-12-17 12:02\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -24,6 +24,10 @@ msgstr ""
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} van de {1} rij(en) geselecteerd."
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# kern} other {# kernen}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "{count, plural, one {{countString} day} other {{countString} days}}"
|
||||
msgstr "{count, plural, one {{countString} dag} other {{countString} dagen}}"
|
||||
@@ -36,6 +40,10 @@ msgstr "{count, plural, one {{countString} uur} other {{countString} uren}}"
|
||||
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/components/routes/system/info-bar.tsx
|
||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||
msgstr "{threads, plural, one {# thread} other {# threads}}"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
msgstr "1 uur"
|
||||
@@ -182,6 +190,11 @@ msgstr "Gemiddelde"
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Gemiddeld CPU-gebruik van containers"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr "Gemiddelde daalt onder <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
@@ -214,7 +227,13 @@ msgstr "Back-ups"
|
||||
msgid "Bandwidth"
|
||||
msgstr "Bandbreedte"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Battery"
|
||||
msgstr "Batterij"
|
||||
|
||||
@@ -230,6 +249,13 @@ msgstr "Inactief geworden"
|
||||
msgid "Before"
|
||||
msgstr "Voor"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Onder {0}{1} in de laatste {2, plural, one {# minuut} other {# minuten}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel ondersteunt OpenID Connect en vele OAuth2 authenticatieaanbieders."
|
||||
@@ -516,7 +542,7 @@ msgstr "Beschrijving"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr ""
|
||||
msgstr "Details"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
@@ -568,7 +594,7 @@ msgstr "Documentatie"
|
||||
|
||||
#. Context: System is down
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
@@ -628,6 +654,10 @@ msgstr "Voer een e-mailadres in..."
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Voer uw eenmalig wachtwoord in."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Tijdelijk"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
@@ -658,6 +688,10 @@ msgstr "Bestaande systemen die niet gedefinieerd zijn in <0>config.yml</0> zulle
|
||||
msgid "Exited active"
|
||||
msgstr "Beëindigd actief"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "Verloopt na één uur of bij hub-herstart."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Exporteren"
|
||||
@@ -720,7 +754,7 @@ msgstr "Vingerafdruk"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr ""
|
||||
msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -793,7 +827,7 @@ msgstr "Als je het wachtwoord voor je beheerdersaccount bent kwijtgeraakt, kan j
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr ""
|
||||
msgstr "Afbeelding"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Inactive"
|
||||
@@ -803,11 +837,6 @@ msgstr "Inactief"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ongeldig e-mailadres."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Kernel"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Taal"
|
||||
@@ -822,12 +851,12 @@ msgstr "Layoutbreedte"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Lifecycle"
|
||||
msgstr ""
|
||||
msgstr "Levenscyclus"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "limit"
|
||||
msgstr ""
|
||||
msgstr "limiet"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Load Average"
|
||||
@@ -900,6 +929,7 @@ msgid "Max 1 min"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -926,7 +956,7 @@ msgstr "Geheugengebruik van docker containers"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
msgstr "Model"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
@@ -1064,7 +1094,7 @@ msgstr "Wachtwoord reset aanvraag ontvangen"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Past"
|
||||
msgstr ""
|
||||
msgstr "Verleden"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Pause"
|
||||
@@ -1087,6 +1117,14 @@ msgstr "Gemiddeld gebruik per kern"
|
||||
msgid "Percentage of time spent in each state"
|
||||
msgstr "Percentage tijd besteed in elke status"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Persistentie"
|
||||
|
||||
#: 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."
|
||||
@@ -1208,7 +1246,7 @@ msgstr "Hervatten"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgctxt "Root disk label"
|
||||
msgid "Root"
|
||||
msgstr ""
|
||||
msgstr "Root"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
@@ -1243,6 +1281,10 @@ msgstr "Instellingen opslaan"
|
||||
msgid "Save system"
|
||||
msgstr "Systeem bewaren"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Opgeslagen in de database en verloopt niet totdat u het uitschakelt."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
msgstr "Schema"
|
||||
@@ -1285,7 +1327,7 @@ msgstr "Servicedetails"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Services"
|
||||
msgstr ""
|
||||
msgstr "Services"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Set percentage thresholds for meter colors."
|
||||
@@ -1317,7 +1359,7 @@ msgstr "Sorteren op"
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Start Time"
|
||||
msgstr ""
|
||||
msgstr "Starttijd"
|
||||
|
||||
#. Context: alert state (active or resolved)
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
@@ -1439,8 +1481,8 @@ msgstr "Tijdnotatie"
|
||||
msgid "To email(s)"
|
||||
msgstr "Naar e-mail(s)"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Schakel raster"
|
||||
|
||||
@@ -1491,7 +1533,7 @@ msgstr "Geactiveerd door"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Triggers"
|
||||
msgstr ""
|
||||
msgstr "Triggers"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when 1 minute load average exceeds a threshold"
|
||||
@@ -1509,6 +1551,10 @@ msgstr "Triggert wanneer de 5 minuten gemiddelde belasting een drempelwaarde ove
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Triggert wanneer een sensor een drempelwaarde overschrijdt"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr "Triggert wanneer de batterijlading onder een drempelwaarde daalt"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Triggert wanneer de gecombineerde up/down een drempelwaarde overschrijdt"
|
||||
@@ -1537,7 +1583,7 @@ msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrij
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
msgstr "Type"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Unit file"
|
||||
@@ -1564,7 +1610,7 @@ msgid "Unlimited"
|
||||
msgstr "Onbeperkt"
|
||||
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "Online"
|
||||
@@ -1591,7 +1637,7 @@ msgstr "Elke 10 minuten bijgewerkt."
|
||||
msgid "Upload"
|
||||
msgstr "Uploaden"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
msgstr "Actief"
|
||||
|
||||
@@ -1663,8 +1709,8 @@ msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Pushmeldingen"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
msgstr "Wanneer ingeschakeld kunnen agenten zich met dit token registreren zonder dat er vooraf een systeem aangemaakt hoeft te worden. Het token verloopt na één uur of bij herstart van de hub."
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "Indien ingeschakeld, stelt deze token agenten in staat zich zelf te registreren zonder voorafgaande systeemcreatie."
|
||||
|
||||
#: 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
Reference in New Issue
Block a user