mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 21:46:18 +01:00
Compare commits
63 Commits
v0.18.2
...
a3198d05a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3198d05a5 | ||
|
|
ddaa2e6ea0 | ||
|
|
be7d4b129e | ||
|
|
a2bcbc112f | ||
|
|
cdb7738164 | ||
|
|
3af3049816 | ||
|
|
bd51580d74 | ||
|
|
76a1145611 | ||
|
|
916503579d | ||
|
|
412f8c6bf5 | ||
|
|
444d080d1d | ||
|
|
54e2e8cfd9 | ||
|
|
902657815c | ||
|
|
94c89a8fdf | ||
|
|
4c92c89fd0 | ||
|
|
9b42dc301a | ||
|
|
f05cce662b | ||
|
|
a3b2530fb4 | ||
|
|
69d9b6c696 | ||
|
|
5da4afb657 | ||
|
|
eafb67ea04 | ||
|
|
ce249b620c | ||
|
|
ce7b56bdd2 | ||
|
|
9cad5a33e5 | ||
|
|
b7b95b9bb0 | ||
|
|
8f5632808a | ||
|
|
002badbe4f | ||
|
|
3b385aff85 | ||
|
|
49a2241033 | ||
|
|
8b1450fe32 | ||
|
|
4964a3c55f | ||
|
|
af57c76fd0 | ||
|
|
e6fc6906d5 | ||
|
|
1aef193b36 | ||
|
|
7522d31ae1 | ||
|
|
761da743b6 | ||
|
|
c779008340 | ||
|
|
3d8db53e52 | ||
|
|
5797f8a6ad | ||
|
|
79ca31d770 | ||
|
|
41f3705b6b | ||
|
|
20324763d2 | ||
|
|
70f85f9590 | ||
|
|
c7f7f51c99 | ||
|
|
6723ec8ea4 | ||
|
|
afc19ebd3b | ||
|
|
c83d00ccaa | ||
|
|
425c8d2bdf | ||
|
|
42da1e5a52 | ||
|
|
afcae025ae | ||
|
|
1de36625a4 | ||
|
|
a2b6c7f5e6 | ||
|
|
799c7b077a | ||
|
|
cb5f944de6 | ||
|
|
23c4958145 | ||
|
|
edb2edc12c | ||
|
|
648a979a81 | ||
|
|
988de6de7b | ||
|
|
031abbfcb3 | ||
|
|
b59fcc26e5 | ||
|
|
acaa9381fe | ||
|
|
8d9e9260e6 | ||
|
|
0fc4a6daed |
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
|
||||
});
|
||||
}
|
||||
@@ -158,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)
|
||||
}
|
||||
|
||||
@@ -335,6 +335,8 @@ func validateCpuPercentage(cpuPct float64, containerName string) error {
|
||||
func updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemory uint64, sent_delta, recv_delta uint64, readTime time.Time) {
|
||||
stats.Cpu = twoDecimals(cpuPct)
|
||||
stats.Mem = bytesToMegabytes(float64(usedMemory))
|
||||
stats.Bandwidth = [2]uint64{sent_delta, recv_delta}
|
||||
// TODO(0.19+): stop populating NetworkSent/NetworkRecv (deprecated in 0.18.3)
|
||||
stats.NetworkSent = bytesToMegabytes(float64(sent_delta))
|
||||
stats.NetworkRecv = bytesToMegabytes(float64(recv_delta))
|
||||
stats.PrevReadTime = readTime
|
||||
@@ -403,6 +405,8 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeM
|
||||
// reset current stats
|
||||
stats.Cpu = 0
|
||||
stats.Mem = 0
|
||||
stats.Bandwidth = [2]uint64{0, 0}
|
||||
// TODO(0.19+): stop populating NetworkSent/NetworkRecv (deprecated in 0.18.3)
|
||||
stats.NetworkSent = 0
|
||||
stats.NetworkRecv = 0
|
||||
|
||||
|
||||
@@ -184,11 +184,12 @@ func TestUpdateContainerStatsValues(t *testing.T) {
|
||||
// Check memory (should be converted to MB: 1048576 bytes = 1 MB)
|
||||
assert.Equal(t, 1.0, stats.Mem)
|
||||
|
||||
// Check network sent (should be converted to MB: 524288 bytes = 0.5 MB)
|
||||
assert.Equal(t, 0.5, stats.NetworkSent)
|
||||
// Check bandwidth (raw bytes)
|
||||
assert.Equal(t, [2]uint64{524288, 262144}, stats.Bandwidth)
|
||||
|
||||
// Check network recv (should be converted to MB: 262144 bytes = 0.25 MB)
|
||||
assert.Equal(t, 0.25, stats.NetworkRecv)
|
||||
// Deprecated fields still populated for backward compatibility with older hubs
|
||||
assert.Equal(t, 0.5, stats.NetworkSent) // 524288 bytes = 0.5 MB
|
||||
assert.Equal(t, 0.25, stats.NetworkRecv) // 262144 bytes = 0.25 MB
|
||||
|
||||
// Check read time
|
||||
assert.Equal(t, testTime, stats.PrevReadTime)
|
||||
@@ -527,8 +528,10 @@ func TestContainerStatsInitialization(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 45.67, stats.Cpu)
|
||||
assert.Equal(t, 2.0, stats.Mem)
|
||||
assert.Equal(t, 1.0, stats.NetworkSent)
|
||||
assert.Equal(t, 0.5, stats.NetworkRecv)
|
||||
assert.Equal(t, [2]uint64{1048576, 524288}, stats.Bandwidth)
|
||||
// Deprecated fields still populated for backward compatibility with older hubs
|
||||
assert.Equal(t, 1.0, stats.NetworkSent) // 1048576 bytes = 1 MB
|
||||
assert.Equal(t, 0.5, stats.NetworkRecv) // 524288 bytes = 0.5 MB
|
||||
assert.Equal(t, testTime, stats.PrevReadTime)
|
||||
}
|
||||
|
||||
@@ -689,6 +692,8 @@ func TestContainerStatsEndToEndWithRealData(t *testing.T) {
|
||||
|
||||
assert.Equal(t, cpuPct, testStats.Cpu)
|
||||
assert.Equal(t, bytesToMegabytes(float64(usedMemory)), testStats.Mem)
|
||||
assert.Equal(t, [2]uint64{1000000, 500000}, testStats.Bandwidth)
|
||||
// Deprecated fields still populated for backward compatibility with older hubs
|
||||
assert.Equal(t, bytesToMegabytes(1000000), testStats.NetworkSent)
|
||||
assert.Equal(t, bytesToMegabytes(500000), testStats.NetworkRecv)
|
||||
assert.Equal(t, testTime, testStats.PrevReadTime)
|
||||
|
||||
25
agent/gpu.go
25
agent/gpu.go
@@ -21,6 +21,7 @@ const (
|
||||
// Commands
|
||||
nvidiaSmiCmd string = "nvidia-smi"
|
||||
rocmSmiCmd string = "rocm-smi"
|
||||
amdgpuCmd string = "amdgpu" // internal cmd for sysfs collection
|
||||
tegraStatsCmd string = "tegrastats"
|
||||
|
||||
// Polling intervals
|
||||
@@ -41,6 +42,7 @@ type GPUManager struct {
|
||||
sync.Mutex
|
||||
nvidiaSmi bool
|
||||
rocmSmi bool
|
||||
amdgpu bool
|
||||
tegrastats bool
|
||||
intelGpuStats bool
|
||||
nvml bool
|
||||
@@ -399,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
|
||||
@@ -408,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
|
||||
@@ -448,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
|
||||
@@ -459,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)
|
||||
}
|
||||
@@ -497,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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
182
agent/smart.go
182
agent/smart.go
@@ -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)
|
||||
|
||||
@@ -144,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -129,11 +129,12 @@ var DockerHealthStrings = map[string]DockerHealth{
|
||||
|
||||
// Docker container stats
|
||||
type Stats struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Cpu float64 `json:"c" cbor:"1,keyasint"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
NetworkSent float64 `json:"ns" cbor:"3,keyasint"`
|
||||
NetworkRecv float64 `json:"nr" cbor:"4,keyasint"`
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Cpu float64 `json:"c" cbor:"1,keyasint"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
NetworkSent float64 `json:"ns,omitzero" cbor:"3,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
|
||||
NetworkRecv float64 `json:"nr,omitzero" cbor:"4,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
|
||||
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"9,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
|
||||
Health DockerHealth `json:"-" cbor:"5,keyasint"`
|
||||
Status string `json:"-" cbor:"6,keyasint"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -27,8 +27,8 @@ type Stats struct {
|
||||
DiskWritePs float64 `json:"dw" cbor:"13,keyasint"`
|
||||
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"14,keyasint,omitempty"`
|
||||
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
NetworkSent float64 `json:"ns" cbor:"16,keyasint"`
|
||||
NetworkRecv float64 `json:"nr" cbor:"17,keyasint"`
|
||||
NetworkSent float64 `json:"ns,omitzero" cbor:"16,keyasint,omitzero"`
|
||||
NetworkRecv float64 `json:"nr,omitzero" cbor:"17,keyasint,omitzero"`
|
||||
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"18,keyasint,omitempty"`
|
||||
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"19,keyasint,omitempty"`
|
||||
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -194,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
|
||||
|
||||
@@ -317,7 +317,11 @@ func createContainerRecords(app core.App, data []*container.Stats, systemId stri
|
||||
params["health"+suffix] = container.Health
|
||||
params["cpu"+suffix] = container.Cpu
|
||||
params["memory"+suffix] = container.Mem
|
||||
params["net"+suffix] = container.NetworkSent + container.NetworkRecv
|
||||
netBytes := container.Bandwidth[0] + container.Bandwidth[1]
|
||||
if netBytes == 0 {
|
||||
netBytes = uint64((container.NetworkSent + container.NetworkRecv) * 1024 * 1024)
|
||||
}
|
||||
params["net"+suffix] = netBytes
|
||||
}
|
||||
queryString := fmt.Sprintf(
|
||||
"INSERT INTO containers (id, system, name, image, status, health, cpu, memory, net, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system = excluded.system, name = excluded.name, image = excluded.image, status = excluded.status, health = excluded.health, cpu = excluded.cpu, memory = excluded.memory, net = excluded.net, updated = excluded.updated",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -461,19 +461,24 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
|
||||
}
|
||||
sums[stat.Name].Cpu += stat.Cpu
|
||||
sums[stat.Name].Mem += stat.Mem
|
||||
sums[stat.Name].NetworkSent += stat.NetworkSent
|
||||
sums[stat.Name].NetworkRecv += stat.NetworkRecv
|
||||
sentBytes := stat.Bandwidth[0]
|
||||
recvBytes := stat.Bandwidth[1]
|
||||
if sentBytes == 0 && recvBytes == 0 && (stat.NetworkSent != 0 || stat.NetworkRecv != 0) {
|
||||
sentBytes = uint64(stat.NetworkSent * 1024 * 1024)
|
||||
recvBytes = uint64(stat.NetworkRecv * 1024 * 1024)
|
||||
}
|
||||
sums[stat.Name].Bandwidth[0] += sentBytes
|
||||
sums[stat.Name].Bandwidth[1] += recvBytes
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]container.Stats, 0, len(sums))
|
||||
for _, value := range sums {
|
||||
result = append(result, container.Stats{
|
||||
Name: value.Name,
|
||||
Cpu: twoDecimals(value.Cpu / count),
|
||||
Mem: twoDecimals(value.Mem / count),
|
||||
NetworkSent: twoDecimals(value.NetworkSent / count),
|
||||
NetworkRecv: twoDecimals(value.NetworkRecv / count),
|
||||
Name: value.Name,
|
||||
Cpu: twoDecimals(value.Cpu / count),
|
||||
Mem: twoDecimals(value.Mem / count),
|
||||
Bandwidth: [2]uint64{uint64(float64(value.Bandwidth[0]) / count), uint64(float64(value.Bandwidth[1]) / count)},
|
||||
})
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { memo, useMemo } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, pinnedAxisDomain, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
pinnedAxisDomain,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { ChartType, Unit } from "@/lib/enums"
|
||||
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
|
||||
@@ -47,27 +54,49 @@ export default memo(function ContainerChart({
|
||||
} else {
|
||||
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
|
||||
obj.tickFormatter = (val) => {
|
||||
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
||||
const { value, unit } = formatBytes(val, isNetChart, chartUnit, !isNetChart)
|
||||
return updateYAxisWidth(`${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`)
|
||||
}
|
||||
}
|
||||
// tooltip formatter
|
||||
if (isNetChart) {
|
||||
const getRxTxBytes = (record?: { b?: [number, number]; ns?: number; nr?: number }) => {
|
||||
if (record?.b?.length && record.b.length >= 2) {
|
||||
return [Number(record.b[0]) || 0, Number(record.b[1]) || 0]
|
||||
}
|
||||
return [(record?.ns ?? 0) * 1024 * 1024, (record?.nr ?? 0) * 1024 * 1024]
|
||||
}
|
||||
const formatRxTx = (recv: number, sent: number) => {
|
||||
const { value: receivedValue, unit: receivedUnit } = formatBytes(recv, true, userSettings.unitNet, false)
|
||||
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, false)
|
||||
return (
|
||||
<span className="flex">
|
||||
{decimalString(receivedValue)} {receivedUnit}
|
||||
<span className="opacity-70 ms-0.5"> rx </span>
|
||||
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
||||
{decimalString(sentValue)} {sentUnit}
|
||||
<span className="opacity-70 ms-0.5"> tx</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
obj.toolTipFormatter = (item: any, key: string) => {
|
||||
try {
|
||||
const sent = item?.payload?.[key]?.ns ?? 0
|
||||
const received = item?.payload?.[key]?.nr ?? 0
|
||||
const { value: receivedValue, unit: receivedUnit } = formatBytes(received, true, userSettings.unitNet, true)
|
||||
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, true)
|
||||
return (
|
||||
<span className="flex">
|
||||
{decimalString(receivedValue)} {receivedUnit}
|
||||
<span className="opacity-70 ms-0.5"> rx </span>
|
||||
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
||||
{decimalString(sentValue)} {sentUnit}
|
||||
<span className="opacity-70 ms-0.5"> tx</span>
|
||||
</span>
|
||||
)
|
||||
if (key === "__total__") {
|
||||
let totalSent = 0
|
||||
let totalRecv = 0
|
||||
const payloadData = item?.payload && typeof item.payload === "object" ? item.payload : {}
|
||||
for (const value of Object.values(payloadData)) {
|
||||
if (!value || typeof value !== "object") {
|
||||
continue
|
||||
}
|
||||
const [sent, recv] = getRxTxBytes(value as { b?: [number, number]; ns?: number; nr?: number })
|
||||
totalSent += sent
|
||||
totalRecv += recv
|
||||
}
|
||||
return formatRxTx(totalRecv, totalSent)
|
||||
}
|
||||
const [sent, recv] = getRxTxBytes(item?.payload?.[key])
|
||||
return formatRxTx(recv, sent)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
@@ -82,7 +111,15 @@ export default memo(function ContainerChart({
|
||||
}
|
||||
// data function
|
||||
if (isNetChart) {
|
||||
obj.dataFunction = (key: string, data: any) => (data[key] ? data[key].nr + data[key].ns : null)
|
||||
obj.dataFunction = (key: string, data: any) => {
|
||||
const payload = data[key]
|
||||
if (!payload) {
|
||||
return null
|
||||
}
|
||||
const sent = payload?.b?.[0] ?? (payload?.ns ?? 0) * 1024 * 1024
|
||||
const recv = payload?.b?.[1] ?? (payload?.nr ?? 0) * 1024 * 1024
|
||||
return sent + recv
|
||||
}
|
||||
} else {
|
||||
obj.dataFunction = (key: string, data: any) => data[key]?.[dataKey] ?? null
|
||||
}
|
||||
@@ -94,11 +131,16 @@ export default memo(function ContainerChart({
|
||||
if (!filter) {
|
||||
return new Set<string>()
|
||||
}
|
||||
const filterTerms = filter.toLowerCase().split(" ").filter(term => term.length > 0)
|
||||
return new Set(Object.keys(chartConfig).filter((key) => {
|
||||
const keyLower = key.toLowerCase()
|
||||
return !filterTerms.some(term => keyLower.includes(term))
|
||||
}))
|
||||
const filterTerms = filter
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((term) => term.length > 0)
|
||||
return new Set(
|
||||
Object.keys(chartConfig).filter((key) => {
|
||||
const keyLower = key.toLowerCase()
|
||||
return !filterTerms.some((term) => keyLower.includes(term))
|
||||
})
|
||||
)
|
||||
}, [chartConfig, filter])
|
||||
|
||||
// console.log('rendered at', new Date())
|
||||
|
||||
@@ -50,10 +50,12 @@ export function useContainerChartConfigs(containerData: ChartData["containerData
|
||||
const currentCpu = totalUsage.cpu.get(containerName) ?? 0
|
||||
const currentMemory = totalUsage.memory.get(containerName) ?? 0
|
||||
const currentNetwork = totalUsage.network.get(containerName) ?? 0
|
||||
const sentBytes = containerStats.b?.[0] ?? (containerStats.ns ?? 0) * 1024 * 1024
|
||||
const recvBytes = containerStats.b?.[1] ?? (containerStats.nr ?? 0) * 1024 * 1024
|
||||
|
||||
totalUsage.cpu.set(containerName, currentCpu + (containerStats.c ?? 0))
|
||||
totalUsage.memory.set(containerName, currentMemory + (containerStats.m ?? 0))
|
||||
totalUsage.network.set(containerName, currentNetwork + (containerStats.nr ?? 0) + (containerStats.ns ?? 0))
|
||||
totalUsage.network.set(containerName, currentNetwork + sentBytes + recvBytes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,19 @@ import { $allSystemsById } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
|
||||
// Unit names and their corresponding number of seconds for converting docker status strings
|
||||
const unitSeconds = [["s", 1], ["mi", 60], ["h", 3600], ["d", 86400], ["w", 604800], ["mo", 2592000]] as const
|
||||
const unitSeconds = [
|
||||
["s", 1],
|
||||
["mi", 60],
|
||||
["h", 3600],
|
||||
["d", 86400],
|
||||
["w", 604800],
|
||||
["mo", 2592000],
|
||||
] as const
|
||||
// Convert docker status string to number of seconds ("Up X minutes", "Up X hours", etc.)
|
||||
function getStatusValue(status: string): number {
|
||||
const [_, num, unit] = status.split(" ")
|
||||
const numValue = Number(num)
|
||||
// Docker uses "a" or "an" instead of "1" for singular units (e.g., "Up a minute", "Up an hour")
|
||||
const numValue = num === "a" || num === "an" ? 1 : Number(num)
|
||||
for (const [unitName, value] of unitSeconds) {
|
||||
if (unit.startsWith(unitName)) {
|
||||
return numValue * value
|
||||
@@ -97,7 +105,7 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Net`} Icon={EthernetIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue() as number
|
||||
const formatted = formatBytes(val, true, undefined, true)
|
||||
const formatted = formatBytes(val, true, undefined, false)
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>
|
||||
)
|
||||
@@ -113,13 +121,14 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
const healthStatus = ContainerHealthLabels[healthValue] || "Unknown"
|
||||
return (
|
||||
<Badge variant="outline" className="dark:border-white/12">
|
||||
<span className={cn("size-2 me-1.5 rounded-full", {
|
||||
"bg-green-500": healthValue === ContainerHealth.Healthy,
|
||||
"bg-red-500": healthValue === ContainerHealth.Unhealthy,
|
||||
"bg-yellow-500": healthValue === ContainerHealth.Starting,
|
||||
"bg-zinc-500": healthValue === ContainerHealth.None,
|
||||
})}>
|
||||
</span>
|
||||
<span
|
||||
className={cn("size-2 me-1.5 rounded-full", {
|
||||
"bg-green-500": healthValue === ContainerHealth.Healthy,
|
||||
"bg-red-500": healthValue === ContainerHealth.Unhealthy,
|
||||
"bg-yellow-500": healthValue === ContainerHealth.Starting,
|
||||
"bg-zinc-500": healthValue === ContainerHealth.None,
|
||||
})}
|
||||
></span>
|
||||
{healthStatus}
|
||||
</Badge>
|
||||
)
|
||||
@@ -129,7 +138,9 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
id: "image",
|
||||
sortingFn: (a, b) => a.original.image.localeCompare(b.original.image),
|
||||
accessorFn: (record) => record.image,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t({ message: "Image", context: "Docker image" })} Icon={LayersIcon} />,
|
||||
header: ({ column }) => (
|
||||
<HeaderButton column={column} name={t({ message: "Image", context: "Docker image" })} Icon={LayersIcon} />
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
return <span className="ms-1.5 xl:w-40 block truncate">{getValue() as string}</span>
|
||||
},
|
||||
@@ -151,20 +162,27 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
const timestamp = getValue() as number
|
||||
return (
|
||||
<span className="ms-1.5 tabular-nums">
|
||||
{hourWithSeconds(new Date(timestamp).toISOString())}
|
||||
</span>
|
||||
)
|
||||
return <span className="ms-1.5 tabular-nums">{hourWithSeconds(new Date(timestamp).toISOString())}</span>
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function HeaderButton({ column, name, Icon }: { column: Column<ContainerRecord>; name: string; Icon: React.ElementType }) {
|
||||
function HeaderButton({
|
||||
column,
|
||||
name,
|
||||
Icon,
|
||||
}: {
|
||||
column: Column<ContainerRecord>
|
||||
name: string
|
||||
Icon: React.ElementType
|
||||
}) {
|
||||
const isSorted = column.getIsSorted()
|
||||
return (
|
||||
<Button
|
||||
className={cn("h-9 px-3 flex items-center gap-2 duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
|
||||
className={cn(
|
||||
"h-9 px-3 flex items-center gap-2 duration-50",
|
||||
isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90"
|
||||
)}
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
@@ -173,4 +191,4 @@ function HeaderButton({ column, name, Icon }: { column: Column<ContainerRecord>;
|
||||
<ArrowUpDownIcon className="size-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
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]) => (
|
||||
|
||||
@@ -25,13 +25,13 @@ const passwordSchema = v.pipe(
|
||||
)
|
||||
|
||||
const LoginSchema = v.looseObject({
|
||||
company_website: honeypot,
|
||||
website: honeypot,
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
})
|
||||
|
||||
const RegisterSchema = v.looseObject({
|
||||
company_website: honeypot,
|
||||
website: honeypot,
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
passwordConfirm: passwordSchema,
|
||||
@@ -248,8 +248,19 @@ export function UserAuthForm({
|
||||
)}
|
||||
<div className="sr-only">
|
||||
{/* honeypot */}
|
||||
<label htmlFor="company_website"></label>
|
||||
<input id="company_website" type="text" name="company_website" 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 ? (
|
||||
@@ -305,9 +316,9 @@ export function UserAuthForm({
|
||||
className="me-2 h-4 w-4 dark:brightness-0 dark:invert"
|
||||
src={getAuthProviderIcon(provider)}
|
||||
alt=""
|
||||
// onError={(e) => {
|
||||
// e.currentTarget.src = "/static/lock.svg"
|
||||
// }}
|
||||
// onError={(e) => {
|
||||
// e.currentTarget.src = "/static/lock.svg"
|
||||
// }}
|
||||
/>
|
||||
)}
|
||||
<span className="translate-y-px">{provider.displayName}</span>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -363,7 +363,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.shiftKey ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey
|
||||
e.metaKey ||
|
||||
e.altKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,18 +94,18 @@ const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
unit?: string
|
||||
filter?: string
|
||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||
truncate?: boolean
|
||||
showTotal?: boolean
|
||||
totalLabel?: React.ReactNode
|
||||
}
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
unit?: string
|
||||
filter?: string
|
||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||
truncate?: boolean
|
||||
showTotal?: boolean
|
||||
totalLabel?: React.ReactNode
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
@@ -139,10 +139,13 @@ const ChartTooltipContent = React.forwardRef<
|
||||
|
||||
React.useMemo(() => {
|
||||
if (filter) {
|
||||
const filterTerms = filter.toLowerCase().split(" ").filter(term => term.length > 0)
|
||||
const filterTerms = filter
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((term) => term.length > 0)
|
||||
payload = payload?.filter((item) => {
|
||||
const itemName = (item.name as string)?.toLowerCase()
|
||||
return filterTerms.some(term => itemName?.includes(term))
|
||||
return filterTerms.some((term) => itemName?.includes(term))
|
||||
})
|
||||
}
|
||||
if (itemSorter) {
|
||||
@@ -158,7 +161,6 @@ const ChartTooltipContent = React.forwardRef<
|
||||
|
||||
let totalValue = 0
|
||||
let hasNumericValue = false
|
||||
const aggregatedNestedValues: Record<string, number> = {}
|
||||
|
||||
for (const item of payload) {
|
||||
const numericValue = typeof item.value === "number" ? item.value : Number(item.value)
|
||||
@@ -166,19 +168,6 @@ const ChartTooltipContent = React.forwardRef<
|
||||
totalValue += numericValue
|
||||
hasNumericValue = true
|
||||
}
|
||||
|
||||
if (content && item?.payload) {
|
||||
const payloadKey = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const nestedPayload = (item.payload as Record<string, unknown> | undefined)?.[payloadKey]
|
||||
|
||||
if (nestedPayload && typeof nestedPayload === "object") {
|
||||
for (const [nestedKey, nestedValue] of Object.entries(nestedPayload)) {
|
||||
if (typeof nestedValue === "number" && Number.isFinite(nestedValue)) {
|
||||
aggregatedNestedValues[nestedKey] = (aggregatedNestedValues[nestedKey] ?? 0) + nestedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNumericValue) {
|
||||
@@ -194,24 +183,11 @@ const ChartTooltipContent = React.forwardRef<
|
||||
}
|
||||
|
||||
if (content) {
|
||||
const basePayload =
|
||||
payload[0]?.payload && typeof payload[0].payload === "object"
|
||||
? { ...(payload[0].payload as Record<string, unknown>) }
|
||||
: {}
|
||||
totalItem.payload = {
|
||||
...basePayload,
|
||||
[totalKey]: aggregatedNestedValues,
|
||||
}
|
||||
totalItem.payload = payload[0]?.payload
|
||||
}
|
||||
|
||||
if (typeof formatter === "function") {
|
||||
return formatter(
|
||||
totalValue,
|
||||
totalName,
|
||||
totalItem,
|
||||
payload.length,
|
||||
totalItem.payload ?? payload[0]?.payload
|
||||
)
|
||||
return formatter(totalValue, totalName, totalItem, payload.length, totalItem.payload ?? payload[0]?.payload)
|
||||
}
|
||||
|
||||
if (content) {
|
||||
@@ -343,11 +319,11 @@ const ChartLegend = RechartsPrimitive.Legend
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
reverse?: boolean
|
||||
}
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
reverse?: boolean
|
||||
}
|
||||
>(({ className, payload, verticalAlign = "bottom", reverse = false }, ref) => {
|
||||
// const { config } = useChart()
|
||||
|
||||
@@ -457,13 +433,16 @@ export {
|
||||
}
|
||||
|
||||
export function pinnedAxisDomain(): AxisDomain {
|
||||
return [0, (dataMax: number) => {
|
||||
if (dataMax > 10) {
|
||||
return Math.round(dataMax)
|
||||
}
|
||||
if (dataMax > 1) {
|
||||
return Math.round(dataMax / 0.1) * 0.1
|
||||
}
|
||||
return dataMax
|
||||
}]
|
||||
}
|
||||
return [
|
||||
0,
|
||||
(dataMax: number) => {
|
||||
if (dataMax > 10) {
|
||||
return Math.round(dataMax)
|
||||
}
|
||||
if (dataMax > 1) {
|
||||
return Math.round(dataMax / 0.1) * 0.1
|
||||
}
|
||||
return dataMax
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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-12-25 19:15\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\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"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "نعم"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: bg\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:17\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Bandwidth на мрежата"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Да"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Настройките за потребителя ти са обновени."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: cs\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Přenos"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Ano"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše uživatelská nastavení byla aktualizována."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: da\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-19 10:55\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -198,7 +198,7 @@ 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>"
|
||||
msgstr "Gennemsnit overstiger <0>{value}{0}</0>"
|
||||
msgstr "Gennemsnittet overstiger <0>{value}{0}</0>"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average power consumption of GPUs"
|
||||
@@ -230,7 +230,7 @@ msgstr "Båndbredde"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -400,7 +400,7 @@ msgstr "Forbindelsen er nede"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Continue"
|
||||
msgstr "Forsæt"
|
||||
msgstr "Fortsæt"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Copied to clipboard"
|
||||
@@ -425,7 +425,7 @@ msgstr "Kopier miljø"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Copy host"
|
||||
msgstr "Kopier host"
|
||||
msgstr "Kopier vært"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -626,7 +626,7 @@ msgstr "Rediger {foo}"
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Email"
|
||||
msgstr "E-mail"
|
||||
msgstr "Email"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Email notifications"
|
||||
@@ -644,11 +644,11 @@ msgstr "Sluttid"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Enter email address to reset password"
|
||||
msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
|
||||
msgstr "Indtast emailadresse for at nulstille adgangskoden"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Enter email address..."
|
||||
msgstr "Indtast e-mailadresse..."
|
||||
msgstr "Indtast emailadresse..."
|
||||
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Enter your one-time password."
|
||||
@@ -791,7 +791,7 @@ msgstr "GPU-enheder"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
msgstr "Gpu Strøm Træk"
|
||||
msgstr "GPU Strøm Træk"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "GPU Usage"
|
||||
@@ -1119,7 +1119,7 @@ msgstr "Procentdel af tid brugt i hver tilstand"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dine brugerindstillinger er opdateret."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Bandbreite"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -598,11 +598,11 @@ msgstr "Dokumentation"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Down"
|
||||
msgstr "Offline"
|
||||
msgstr "Inaktiv"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Down ({downSystemsLength})"
|
||||
msgstr "Offline ({downSystemsLength})"
|
||||
msgstr "Inaktiv ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
@@ -969,7 +969,7 @@ msgstr "Name"
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Net"
|
||||
msgstr "Netz"
|
||||
msgstr "Netzwerk"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network traffic of docker containers"
|
||||
@@ -1119,7 +1119,7 @@ msgstr "Prozentsatz der Zeit in jedem Zustand"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
@@ -1613,11 +1613,11 @@ msgstr "Unbegrenzt"
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Up"
|
||||
msgstr "aktiv"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Up ({upSystemsLength})"
|
||||
msgstr "aktiv ({upSystemsLength})"
|
||||
msgstr "Aktiv ({upSystemsLength})"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Update"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-14 09:39\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -1307,7 +1307,7 @@ msgstr "Buscar sistemas o configuraciones..."
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "See <0>notification settings</0> to configure how you receive alerts."
|
||||
msgstr "Consulta <0>configuración de notificaciones</0> para configurar cómo recibe alertas."
|
||||
msgstr "Consulta la <0>configuración de notificaciones</0> para configurar cómo recibes alertas."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Select {foo}"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Sí"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Tu configuración de usuario ha sido actualizada."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fa\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Persian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "بله"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-01-09 21:08\n"
|
||||
"PO-Revision-Date: 2026-02-02 15:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@@ -157,6 +157,7 @@ msgstr "Alertes"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Tous les conteneurs"
|
||||
@@ -837,6 +838,7 @@ msgstr "Inactif"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Adresse email invalide."
|
||||
|
||||
#: src/components/lang-toggle.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
@@ -1335,6 +1337,7 @@ msgstr "Définir des seuils de pourcentage pour les couleurs des compteurs."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings"
|
||||
@@ -1486,6 +1489,7 @@ msgstr "Aux email(s)"
|
||||
msgid "Toggle grid"
|
||||
msgstr "Basculer la grille"
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
msgstr "Changer le thème"
|
||||
@@ -1741,3 +1745,4 @@ msgstr "Oui"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vos paramètres utilisateur ont été mis à jour."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: he\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\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"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "כן"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "הגדרות המשתמש שלך עודכנו."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\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"
|
||||
@@ -99,7 +99,7 @@ msgstr "Aktivan"
|
||||
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Active Alerts"
|
||||
msgstr "Aktivna upozorenja"
|
||||
msgstr "Aktivna Upozorenja"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Active state"
|
||||
@@ -113,11 +113,11 @@ msgstr "Dodaj {foo}"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
msgstr "Dodaj <0>Sistem</0>"
|
||||
msgstr "Dodaj <0>Sustav</0>"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add system"
|
||||
msgstr "Dodaj sistem"
|
||||
msgstr "Dodaj sustav"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Add URL"
|
||||
@@ -125,7 +125,7 @@ msgstr "Dodaj URL"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Podesite opcije prikaza za grafikone."
|
||||
msgstr "Podesite opcije prikaza grafikona."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust the width of the main layout"
|
||||
@@ -168,7 +168,7 @@ msgstr "Svi spremnici"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "All Systems"
|
||||
msgstr "Svi Sistemi"
|
||||
msgstr "Svi Sustavi"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Are you sure you want to delete {name}?"
|
||||
@@ -206,7 +206,7 @@ msgstr "Prosječna potrošnja energije grafičkog procesora"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average system-wide CPU utilization"
|
||||
msgstr "Prosječna iskorištenost procesora na cijelom sustavu"
|
||||
msgstr "Prosječna iskorištenost procesora u cijelom sustavu"
|
||||
|
||||
#. placeholder {0}: gpu.n
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -215,7 +215,7 @@ msgstr "Prosječna iskorištenost {0}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average utilization of GPU engines"
|
||||
msgstr "Prosječna iskorištenost GPU motora"
|
||||
msgstr "Prosječna iskorištenost grafičkih procesora"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
@@ -225,12 +225,12 @@ msgstr "Sigurnosne kopije"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Bandwidth"
|
||||
msgstr "Propusnost"
|
||||
msgstr "Mrežna Propusnost"
|
||||
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -258,11 +258,11 @@ msgstr "Ispod {0}{1} u posljednjih {2, plural, one {# minuti} few {# minute} oth
|
||||
|
||||
#: 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."
|
||||
msgstr "Beszel podržava OpenID Connect i mnoge druge pružatelje OAuth2 autentifikacije."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
|
||||
msgstr "Beszel koristi <0>Shoutrrr</0> za integraciju sa popularnim servisima za notifikacije."
|
||||
msgstr "Beszel koristi <0>Shoutrrr</0> za integraciju s popularnim obavještajnim uslugama."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Binary"
|
||||
@@ -339,19 +339,19 @@ msgstr "Puni se"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Chart options"
|
||||
msgstr "Opcije grafikona"
|
||||
msgstr "Postavke grafikona"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Check {email} for a reset link."
|
||||
msgstr "Provjerite {email} za vezu za resetiranje."
|
||||
msgstr "Provjerite {email} za pristup poveznici za resetiranje."
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Check logs for more details."
|
||||
msgstr "Provjerite logove za više detalja."
|
||||
msgstr "Provjerite zapise (logove) za više detalja."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Check your notification service"
|
||||
msgstr "Provjerite Vaš servis notifikacija"
|
||||
msgstr "Provjerite svoju obavještajnu uslugu"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
@@ -387,7 +387,7 @@ msgstr "Konfigurirajte način primanja obavijesti upozorenja."
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Potvrdite lozinku"
|
||||
msgstr "Potvrdi lozinku"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Conflicts"
|
||||
@@ -400,7 +400,7 @@ msgstr "Veza je pala"
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Continue"
|
||||
msgstr "Nastavite"
|
||||
msgstr "Nastavi"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Copied to clipboard"
|
||||
@@ -577,15 +577,15 @@ msgstr "Iskorištenost diska od {extraFsName}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker CPU Usage"
|
||||
msgstr "Iskorištenost Docker Procesora"
|
||||
msgstr "Iskorištenost Docker procesora"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Memory Usage"
|
||||
msgstr "Iskorištenost Docker Memorije"
|
||||
msgstr "Iskorištenost Docker memorije"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Network I/O"
|
||||
msgstr "Docker Mrežni I/O"
|
||||
msgstr "Docker mrežni I/O"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
@@ -630,12 +630,12 @@ msgstr "Email"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Email notifications"
|
||||
msgstr "Email notifikacije"
|
||||
msgstr "Email obavijesti"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Empty"
|
||||
msgstr "Prazna"
|
||||
msgstr "Prazno"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
@@ -644,7 +644,7 @@ msgstr "Vrijeme završetka"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Enter email address to reset password"
|
||||
msgstr "Unesite email adresu za resetiranje lozinke"
|
||||
msgstr "Unesite email adresu kako biste resetirali lozinku"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Enter email address..."
|
||||
@@ -652,7 +652,7 @@ msgstr "Unesite email adresu..."
|
||||
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Enter your one-time password."
|
||||
msgstr "Unesite Vašu jednokratnu lozinku."
|
||||
msgstr "Unesite jednokratnu lozinku."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
@@ -682,7 +682,7 @@ msgstr "Glavni PID izvršavanja"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
|
||||
msgstr "Postojeći sistemi koji nisu definirani u <0>config.yml</0> će biti izbrisani. Molimo Vas napravite redovite sigurnosne kopije."
|
||||
msgstr "Postojeći sustavi koji nisu definirani u <0>config.yml</0> datoteci bit će izbrisani. Molimo Vas da spremate redovite sigurnosne kopije."
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Exited active"
|
||||
@@ -694,7 +694,7 @@ msgstr "Istječe nakon jednog sata ili ponovnog pokretanja huba."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Izvezi"
|
||||
msgstr "Izvoz"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
@@ -718,21 +718,21 @@ msgstr "Neuspjeli atributi:"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Provjera autentičnosti nije uspjela"
|
||||
msgstr "Neuspješna provjera autentičnosti"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Failed to save settings"
|
||||
msgstr "Neuspješno snimanje postavki"
|
||||
msgstr "Neuspješno spremanje postavki"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Failed to send test notification"
|
||||
msgstr "Neuspješno slanje testne notifikacije"
|
||||
msgstr "Neuspješno slanje probne obavijesti"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Ažuriranje upozorenja nije uspjelo"
|
||||
msgstr "Neuspješno ažuriranje upozorenja"
|
||||
|
||||
#. placeholder {0}: statusTotals[ServiceStatus.Failed]
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
@@ -750,7 +750,7 @@ msgstr "Filtriraj..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Otisak prsta"
|
||||
msgstr "Otisak"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
@@ -773,7 +773,7 @@ msgstr "FreeBSD naredba"
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Full"
|
||||
msgstr "Puna"
|
||||
msgstr "Puno"
|
||||
|
||||
#. Context: General settings
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -787,7 +787,7 @@ msgstr "Globalno"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Engines"
|
||||
msgstr "GPU motori"
|
||||
msgstr "Grafički procesori"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Power Draw"
|
||||
@@ -799,7 +799,7 @@ msgstr "Iskorištenost GPU-a"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Grid"
|
||||
msgstr "Mreža"
|
||||
msgstr "Rešetka"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
@@ -818,7 +818,7 @@ msgstr "Host / IP"
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Idle"
|
||||
msgstr "Neaktivna"
|
||||
msgstr "Neaktivno"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
@@ -835,7 +835,7 @@ msgstr "Neaktivno"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Nevažeća adresa e-pošte."
|
||||
msgstr "Nevažeća email adresa."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
@@ -877,7 +877,7 @@ msgstr "Prosječno Opterećenje 5m"
|
||||
#. Short label for load average
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Load Avg"
|
||||
msgstr "Prosječno opterećenje"
|
||||
msgstr "Prosječno Opterećenje"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Load state"
|
||||
@@ -898,13 +898,13 @@ msgstr "Prijava"
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Login attempt failed"
|
||||
msgstr "Pokušaj prijave nije uspio"
|
||||
msgstr "Neuspješno pokušaj prijave"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Logs"
|
||||
msgstr "Logovi"
|
||||
msgstr "Zapisi"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
@@ -948,15 +948,15 @@ msgstr "Vrhunac memorije"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Memory Usage"
|
||||
msgstr "Upotreba memorije"
|
||||
msgstr "Iskorištenost memorije"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Upotreba memorije Docker spremnika"
|
||||
msgstr "Iskorištenost memorije Docker spremnika"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Model"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
@@ -1028,7 +1028,7 @@ msgstr "Podrška za OAuth 2 / OIDC"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
|
||||
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka biti će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
|
||||
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka bit će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
@@ -1045,11 +1045,11 @@ msgstr "Jednokratna lozinka"
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Open menu"
|
||||
msgstr "Otvori menu"
|
||||
msgstr "Otvori meni"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Or continue with"
|
||||
msgstr "Ili nastavi sa"
|
||||
msgstr "Ili nastavi s"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "Other"
|
||||
@@ -1057,7 +1057,7 @@ msgstr "Ostalo"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Prebrišite postojeća upozorenja"
|
||||
msgstr "Prebriši postojeća upozorenja"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
@@ -1090,7 +1090,7 @@ msgstr "Lozinka mora biti kraća od 72 bajta."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Password reset request received"
|
||||
msgstr "Zahtjev za ponovno postavljanje lozinke primljen"
|
||||
msgstr "Zahtjev za ponovno postavljanje lozinke zaprimljen"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Past"
|
||||
@@ -1131,20 +1131,20 @@ msgstr "Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku u
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Please check logs for more details."
|
||||
msgstr "Za više detalja provjerite logove."
|
||||
msgstr "Za više detalja provjerite zapise (logove)."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Please check your credentials and try again"
|
||||
msgstr "Provjerite svoje podatke i pokušajte ponovno"
|
||||
msgstr "Provjerite svoje vjerodajnice i pokušajte ponovno"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Please create an admin account"
|
||||
msgstr "Molimo kreirajte administratorski račun"
|
||||
msgstr "Molimo kreirajte administrativan račun"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Please enable pop-ups for this site"
|
||||
msgstr "Omogućite skočne prozore za ovu stranicu"
|
||||
msgstr "Molimo omogućite skočne prozore za ovu stranicu"
|
||||
|
||||
#: src/lib/api.ts
|
||||
msgid "Please log in again"
|
||||
@@ -1152,7 +1152,7 @@ msgstr "Molimo prijavite se ponovno"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Please see <0>the documentation</0> for instructions."
|
||||
msgstr "Molimo pogledajte <0>dokumentaciju</0> za instrukcije."
|
||||
msgstr "Molimo provjerite <0>dokumentaciju</0> za upute."
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Please sign in to your account"
|
||||
@@ -1412,7 +1412,7 @@ msgstr "Systemd servisi"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Systems"
|
||||
msgstr "Sistemi"
|
||||
msgstr "Sustavi"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Da"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše korisničke postavke su ažurirane."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: hu\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -113,7 +113,7 @@ msgstr "Hozzáadás {foo}"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
msgstr "Hozzáadás <0>System</0>"
|
||||
msgstr "<0>Rendszer</0> Hozzáadása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add system"
|
||||
@@ -125,7 +125,7 @@ msgstr "URL hozzáadása"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Állítsa be a diagram megjelenítését."
|
||||
msgstr "A diagramok megjelenítésének beállítása."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust the width of the main layout"
|
||||
@@ -159,7 +159,7 @@ msgstr "Riasztások"
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Összes konténer"
|
||||
msgstr "Minden konténer"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
@@ -230,7 +230,7 @@ msgstr "Sávszélesség"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Akk"
|
||||
msgstr "Akku"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -656,7 +656,7 @@ msgstr "Adja meg az egyszeri jelszavát."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Átmeneti"
|
||||
msgstr "Ideiglenes"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
@@ -758,7 +758,7 @@ msgstr "Firmware"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "A <0>{min}</0> {min, plural, one {perc} other {percek}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {percig} other {percig}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Forgot password?"
|
||||
@@ -1123,7 +1123,7 @@ msgstr "Állandó"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
msgstr "Kitartás"
|
||||
msgstr "Tartósság"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
@@ -1311,7 +1311,7 @@ msgstr "Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogy
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Select {foo}"
|
||||
msgstr ""
|
||||
msgstr "{foo} kiválasztása"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Sent"
|
||||
@@ -1549,19 +1549,19 @@ msgstr "Riaszt, ha az 5 perces terhelési átlag túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
|
||||
msgstr "Riaszt, ha bármelyik hőmérséklet é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 ""
|
||||
msgstr "Riaszt, ha az akkumulátor töltöttségi szintje egy küszöbérték alá esik"
|
||||
|
||||
#: 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"
|
||||
msgstr "Riaszt, ha a sávszélesség-használat túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when CPU usage exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha a CPU érzékelő túllép egy küszöbértéket"
|
||||
msgstr "Riaszt, ha a CPU használat túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when GPU usage exceeds a threshold"
|
||||
@@ -1569,15 +1569,15 @@ msgstr "Riaszt, ha a GPU használat túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when memory usage exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha a Ram érzékelő túllép egy küszöbértéket"
|
||||
msgstr "Riaszt, ha a memóriahasználat túllép egy küszöbértéket"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when status switches between up and down"
|
||||
msgstr "Bekapcsol, amikor az állapot fel és le között változik"
|
||||
msgstr "Riaszt, amikor a rendszer online állapota változik"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha a lemez érzékelő túllép egy küszöbértéket"
|
||||
msgstr "Riaszt, ha a lemezhasználat túllép egy küszöbértéket"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
@@ -1710,7 +1710,7 @@ 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."
|
||||
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."
|
||||
msgstr "Ha engedélyezve van, ez a token lehetővé teszi az ügynökök számára a regisztrációt a rendszer előzetes létrehozása nélkül."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Igen"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "A felhasználói beállítások frissítésre kerültek."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: id\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-25 10:45\n"
|
||||
"PO-Revision-Date: 2026-01-31 22:39\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Indonesian\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -157,6 +157,7 @@ msgstr "Peringatan"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Semua Container"
|
||||
@@ -456,7 +457,7 @@ msgstr "Salin YAML"
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
@@ -542,7 +543,7 @@ msgstr "Deskripsi"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Detail"
|
||||
msgstr "Detail"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Device"
|
||||
@@ -555,11 +556,11 @@ msgstr "Sedang tidak di charge"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Disk I/O"
|
||||
msgstr "Disk I/O"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Disk unit"
|
||||
@@ -585,7 +586,7 @@ msgstr "Penggunaan Memori Docker"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Network I/O"
|
||||
msgstr "Docker Network I/O"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
@@ -606,7 +607,7 @@ msgstr "Mati ({downSystemsLength})"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
msgid "Duration"
|
||||
@@ -626,7 +627,7 @@ msgstr "Ubah {foo}"
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Email notifications"
|
||||
@@ -667,7 +668,7 @@ msgstr "Sementara"
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#. placeholder {1}: info.unit
|
||||
@@ -694,7 +695,7 @@ msgstr "Kedaluwarsa setelah satu jam atau saat restart hub."
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
msgstr "Ekspor"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Export configuration"
|
||||
@@ -706,7 +707,7 @@ msgstr "Export konfigurasi sistem anda saat ini."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Failed"
|
||||
@@ -746,15 +747,15 @@ msgstr "Gagal: {0}"
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Fingerprint"
|
||||
msgstr "Sidik jari"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
msgstr "Firmware"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -783,7 +784,7 @@ msgstr "Umum"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Global"
|
||||
msgstr "Global"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "GPU Engines"
|
||||
@@ -813,12 +814,12 @@ msgstr "Perintah Homebrew"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
msgstr "Host / IP"
|
||||
msgstr ""
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
msgid "Idle"
|
||||
msgstr "Idle"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
@@ -827,7 +828,7 @@ msgstr "Jika anda kehilangan kata sandi untuk akun admin anda, anda dapat merese
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Image"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Inactive"
|
||||
@@ -837,6 +838,7 @@ msgstr "Tidak aktif"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Alamat email tidak valid."
|
||||
|
||||
#: src/components/lang-toggle.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Bahasa"
|
||||
@@ -889,7 +891,7 @@ msgstr "Memuat..."
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Log Out"
|
||||
msgstr "Keluar"
|
||||
|
||||
#: src/components/login/login.tsx
|
||||
msgid "Login"
|
||||
@@ -956,7 +958,7 @@ msgstr "Penggunaan memori kontainer docker"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Model"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
@@ -1160,7 +1162,7 @@ msgstr "Silakan masuk ke akun anda"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
msgstr ""
|
||||
|
||||
#. Power On Time
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
@@ -1246,7 +1248,7 @@ msgstr "Lanjutkan"
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgctxt "Root disk label"
|
||||
msgid "Root"
|
||||
msgstr "Root"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
@@ -1335,6 +1337,7 @@ msgstr "Tetapkan ambang persentase untuk warna meter."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings"
|
||||
@@ -1374,7 +1377,7 @@ msgstr "Status"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
msgid "Sub State"
|
||||
@@ -1486,6 +1489,7 @@ msgstr "Ke email"
|
||||
msgid "Toggle grid"
|
||||
msgstr "Ganti tampilan"
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
msgstr "Ganti tema"
|
||||
@@ -1493,7 +1497,7 @@ msgstr "Ganti tema"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -1512,7 +1516,7 @@ msgstr "Token dan Fingerprint digunakan untuk mengautentikasi koneksi WebSocket
|
||||
#: src/components/ui/chart.tsx
|
||||
#: src/components/ui/chart.tsx
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Total data received for each interface"
|
||||
@@ -1525,7 +1529,7 @@ msgstr "Total data yang dikirim untuk setiap antarmuka"
|
||||
#. placeholder {0}: data.length
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Total: {0}"
|
||||
msgstr "Total: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Triggered by"
|
||||
@@ -1635,7 +1639,7 @@ msgstr "Diperbarui setiap 10 menit."
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Upload"
|
||||
msgstr "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Uptime"
|
||||
@@ -1741,3 +1745,4 @@ msgstr "Ya"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Pengaturan pengguna anda telah diperbarui."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:17\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Larghezza di banda"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Sì"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Le impostazioni utente sono state aggiornate."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Japanese\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "はい"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "ユーザー設定が更新されました。"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ko\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-02-02 12:39\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Korean\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -157,6 +157,7 @@ msgstr "알림"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "모든 컨테이너"
|
||||
@@ -678,7 +679,7 @@ msgstr "마지막 {2, plural, one {# 분} other {# 분}} 동안 {0}{1} 초과"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Exec main PID"
|
||||
msgstr ""
|
||||
msgstr "Exec 주 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."
|
||||
@@ -837,6 +838,7 @@ msgstr "비활성"
|
||||
msgid "Invalid email address."
|
||||
msgstr "잘못된 이메일 주소입니다."
|
||||
|
||||
#: src/components/lang-toggle.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "언어"
|
||||
@@ -856,7 +858,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"
|
||||
@@ -912,7 +914,7 @@ msgstr "알림을 생성하려 하시나요? 시스템 테이블의 종 <0/> 아
|
||||
|
||||
#: 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."
|
||||
@@ -1246,7 +1248,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"
|
||||
@@ -1335,6 +1337,7 @@ msgstr "그래프 미터 색상의 백분율 임계값을 설정합니다."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings"
|
||||
@@ -1486,6 +1489,7 @@ msgstr "받는사람(들)"
|
||||
msgid "Toggle grid"
|
||||
msgstr "그리드 전환"
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
msgstr "테마 전환"
|
||||
@@ -1587,7 +1591,7 @@ msgstr "유형"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Unit file"
|
||||
msgstr ""
|
||||
msgstr "Unit 파일"
|
||||
|
||||
#. Temperature / network units
|
||||
#: src/components/routes/settings/general.tsx
|
||||
@@ -1741,3 +1745,4 @@ msgstr "예"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "사용자 설정이 업데이트되었습니다."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: nl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-17 12:02\n"
|
||||
"PO-Revision-Date: 2026-02-06 13:42\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -59,7 +59,7 @@ msgstr "1 minuut"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 week"
|
||||
msgstr ""
|
||||
msgstr "1 week"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "12 hours"
|
||||
@@ -134,7 +134,7 @@ msgstr "Breedte van het hoofdlayout aanpassen"
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
msgstr "Administrator"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "After"
|
||||
@@ -142,7 +142,7 @@ msgstr "Na"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
msgstr ""
|
||||
msgstr "Agent"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
@@ -157,6 +157,7 @@ msgstr "Waarschuwingen"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Alle containers"
|
||||
@@ -271,7 +272,7 @@ msgstr "Binair"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bits (Kbps, Mbps, Gbps)"
|
||||
msgstr ""
|
||||
msgstr "Bits (Kbps, Mbps, Gbps)"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Boot state"
|
||||
@@ -280,11 +281,11 @@ msgstr "Opstartstatus"
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
msgstr ""
|
||||
msgstr "Bytes (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
msgstr ""
|
||||
msgstr "Cache / Buffers"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Can reload"
|
||||
@@ -318,7 +319,7 @@ msgstr "Opgelet - potentieel gegevensverlies"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Celsius (°C)"
|
||||
msgstr ""
|
||||
msgstr "Celsius (°C)"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change display units for metrics."
|
||||
@@ -456,7 +457,7 @@ msgstr "YAML kopiëren"
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr ""
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
@@ -706,7 +707,7 @@ msgstr "Exporteer je huidige systeemconfiguratie."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
msgstr "Fahrenheit (°F)"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Failed"
|
||||
@@ -837,6 +838,7 @@ msgstr "Inactief"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Ongeldig e-mailadres."
|
||||
|
||||
#: src/components/lang-toggle.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Taal"
|
||||
@@ -926,7 +928,7 @@ msgstr "Handmatige installatie-instructies"
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Max 1 min"
|
||||
msgstr ""
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
@@ -1119,7 +1121,7 @@ msgstr "Percentage tijd besteed in elke status"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
msgstr "Blijvend"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
@@ -1335,6 +1337,7 @@ msgstr "Stel percentagedrempels in voor meterkleuren."
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings"
|
||||
@@ -1374,7 +1377,7 @@ msgstr "Status"
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
msgstr "Status"
|
||||
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
msgid "Sub State"
|
||||
@@ -1447,7 +1450,7 @@ msgstr "Temperatuur van systeem sensoren"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Test <0>URL</0>"
|
||||
msgstr ""
|
||||
msgstr "Test <0>URL</0>"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Test notification sent"
|
||||
@@ -1486,6 +1489,7 @@ msgstr "Naar e-mail(s)"
|
||||
msgid "Toggle grid"
|
||||
msgstr "Schakel raster"
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
msgstr "Schakel thema"
|
||||
@@ -1741,3 +1745,4 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: no\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Norwegian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Båndbredde"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr ""
|
||||
msgstr "Batteri"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -908,7 +908,7 @@ msgstr "Logger"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
msgstr "Ser du etter hvor du kan opprette alarmer? Klikk på bjelle-ikonene <0/> i systemtabellen."
|
||||
msgstr "Ser du etter hvor du kan opprette alarmer? Klikk på bjelle-ikonet <0/> for systemet i systemoversikten."
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Main PID"
|
||||
@@ -1549,7 +1549,7 @@ msgstr "Slår inn når gjennomsnittsbelastningen over 5 minutter overstiger en g
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Slår inn når enhver sensor overstiger en grenseverdi"
|
||||
msgstr "Slår inn når hvilken som helst sensor overstiger en grenseverdi"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-18 19:21\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
@@ -129,7 +129,7 @@ msgstr "Dostosuj opcje wyświetlania wykresów."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Adjust the width of the main layout"
|
||||
msgstr "Dostosuj szerokość głównego układu"
|
||||
msgstr "Dostosuj szerokość widoku"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
@@ -188,12 +188,12 @@ msgstr "Średnia"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Średnie wykorzystanie procesora przez kontenery"
|
||||
msgstr "Średnie wykorzystanie CPU przez kontenery"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Average drops below <0>{value}{0}</0>"
|
||||
msgstr ""
|
||||
msgstr "Średnia spada poniżej <0>{value}{0}</0>"
|
||||
|
||||
#. placeholder {0}: alertData.unit
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
@@ -206,7 +206,7 @@ msgstr "Średnie zużycie energii przez GPU"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Average system-wide CPU utilization"
|
||||
msgstr "Średnie wykorzystanie procesora w całym systemie"
|
||||
msgstr "Średnie wykorzystanie CPU w całym systemie"
|
||||
|
||||
#. placeholder {0}: gpu.n
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -220,7 +220,7 @@ msgstr "Średnie wykorzystanie silników GPU"
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Backups"
|
||||
msgstr "Kopie"
|
||||
msgstr "Kopie zapasowe"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -230,7 +230,7 @@ msgstr "Przepustowość"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr ""
|
||||
msgstr "Bateria"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -254,7 +254,7 @@ msgstr "Przed"
|
||||
#. placeholder {2}: alert.min
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr ""
|
||||
msgstr "Poniżej {0}{1} w ciągu ostatnich {2, plural, one {# minuty} other {# minut}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
@@ -262,7 +262,7 @@ msgstr "Beszel obsługuje OpenID Connect i wielu dostawców uwierzytelniania OAu
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
|
||||
msgstr "Beszel używa <0>Shoutrrr</0> do integracji z popularnych serwisów powiadomień."
|
||||
msgstr "Beszel używa <0>Shoutrrr</0> do integracji z popularnymi serwisami powiadomień."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Binary"
|
||||
@@ -314,7 +314,7 @@ msgstr "Pojemność"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Caution - potential data loss"
|
||||
msgstr "Uwaga- potencjalna utrata danych."
|
||||
msgstr "Uwaga - ryzyko utraty danych"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Celsius (°C)"
|
||||
@@ -326,7 +326,7 @@ msgstr "Zmień jednostki wyświetlania dla metryk."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Change general application options."
|
||||
msgstr "Zmiana ogólnych ustawień aplikacji."
|
||||
msgstr "Zmień ogólne ustawienia aplikacji."
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Charge"
|
||||
@@ -339,7 +339,7 @@ msgstr "Ładuje się"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Chart options"
|
||||
msgstr "Opcje wykresu"
|
||||
msgstr "Wykresy"
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Check {email} for a reset link."
|
||||
@@ -361,15 +361,15 @@ msgstr "Wyczyść"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Kliknij na kontener, aby wyświetlić więcej informacji."
|
||||
msgstr "Wybierz kontener, aby wyświetlić więcej informacji."
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Click on a device to view more information."
|
||||
msgstr "Kliknij na urządzenie, aby wyświetlić więcej informacji."
|
||||
msgstr "Wybierz urządzenie, aby wyświetlić więcej informacji."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "Click on a system to view more information."
|
||||
msgstr "Kliknij na system, aby zobaczyć więcej informacji."
|
||||
msgstr "Wybierz system, aby wyświetlić więcej informacji."
|
||||
|
||||
#: src/components/ui/input-copy.tsx
|
||||
msgid "Click to copy"
|
||||
@@ -500,11 +500,11 @@ msgstr "Krytyczny (%)"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Cumulative Download"
|
||||
msgstr "Pobieranie skumulowane"
|
||||
msgstr "Pobieranie łącznie"
|
||||
|
||||
#: src/components/routes/system/network-sheet.tsx
|
||||
msgid "Cumulative Upload"
|
||||
msgstr "Wysyłanie skumulowane"
|
||||
msgstr "Wysyłanie łącznie"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/components/routes/system.tsx
|
||||
@@ -577,11 +577,11 @@ msgstr "Wykorzystanie dysku {extraFsName}"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker CPU Usage"
|
||||
msgstr "Wykorzystanie procesora przez Docker"
|
||||
msgstr "Użycie CPU przez Docker"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Memory Usage"
|
||||
msgstr "Wykorzystanie pamięci przez Docker"
|
||||
msgstr "Użycie pamięci przez Docker"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Docker Network I/O"
|
||||
@@ -656,7 +656,7 @@ msgstr "Wprowadź swoje jednorazowe hasło."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Ephemeral"
|
||||
msgstr "Efemeryczny"
|
||||
msgstr "Tymczasowy"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
@@ -682,7 +682,7 @@ msgstr "Główny PID wykonania"
|
||||
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
msgid "Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups."
|
||||
msgstr "Istniejące systemy, które nie są zdefiniowane w <0>config.yml</0>, zostaną usunięte. Proszę regularnie tworzyć kopie zapasowe."
|
||||
msgstr "Istniejące systemy, które nie są zdefiniowane w <0>config.yml</0>, zostaną usunięte. Pamiętaj aby regularnie tworzyć kopie zapasowe."
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Exited active"
|
||||
@@ -728,7 +728,7 @@ msgstr "Nie udało się zapisać ustawień"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Failed to send test notification"
|
||||
msgstr "Nie udało się wysłać testowego powiadomienia"
|
||||
msgstr "Nie udało się wysłać powiadomienia testowego"
|
||||
|
||||
#: src/components/alerts/alerts-sheet.tsx
|
||||
msgid "Failed to update alert"
|
||||
@@ -803,7 +803,7 @@ msgstr "Siatka"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgid "Health"
|
||||
msgstr "Zdrowie"
|
||||
msgstr "Kondycja"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -864,15 +864,15 @@ msgstr "Średnie obciążenie"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr "Średnie obciążenie 15 m"
|
||||
msgstr "Średnie obciążenie 15 min"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 1m"
|
||||
msgstr "Średnie obciążenie 1 m"
|
||||
msgstr "Średnie obciążenie 1 min"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr "Średnie obciążenie 5 m"
|
||||
msgstr "Średnie obciążenie 5 min"
|
||||
|
||||
#. Short label for load average
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -948,7 +948,7 @@ msgstr "Szczyt pamięci"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Memory Usage"
|
||||
msgstr "Wykorzystanie pamięci"
|
||||
msgstr "Użycie pamięci"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Memory usage of docker containers"
|
||||
@@ -996,7 +996,7 @@ msgstr "Nie"
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "No results found."
|
||||
msgstr "Brak wyników."
|
||||
msgstr "Nie znaleziono wyników."
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
@@ -1082,7 +1082,7 @@ msgstr "Hasło"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be at least 8 characters."
|
||||
msgstr "Hasło musi mieć co najmniej 8 znaków."
|
||||
msgstr "Hasło musi zawierać co najmniej 8 znaków."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Password must be less than 72 bytes."
|
||||
@@ -1090,11 +1090,11 @@ msgstr "Hasło musi być mniejsze niż 72 bajty."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
msgid "Password reset request received"
|
||||
msgstr "Otrzymane żądanie resetowania hasła"
|
||||
msgstr "Otrzymano żądanie resetowania hasła"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Past"
|
||||
msgstr "Przeszłe"
|
||||
msgstr "Poprzednie"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Pause"
|
||||
@@ -1237,7 +1237,7 @@ msgstr "Rozwiązany"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Restarts"
|
||||
msgstr "Ponowne uruchomienia"
|
||||
msgstr "Uruchamia ponownie"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Resume"
|
||||
@@ -1283,7 +1283,7 @@ msgstr "Zapisz system"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "Zapisane w bazie danych i nie wygasa, dopóki go nie wyłączysz."
|
||||
msgstr "Zapisany w bazie danych. Nie wygasa, dopóki go nie wyłączysz."
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
@@ -1342,7 +1342,7 @@ msgstr "Ustawienia"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings saved"
|
||||
msgstr "Ustawienia zapisane"
|
||||
msgstr "Ustawienia zostały zapisane"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Sign in"
|
||||
@@ -1484,7 +1484,7 @@ msgstr "Do e-mail(ów)"
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "Toggle grid"
|
||||
msgstr "Przełącz siatkę"
|
||||
msgstr "Przełącz widok"
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
@@ -1553,7 +1553,7 @@ msgstr "Wyzwalane, gdy jakikolwiek czujnik przekroczy ustalony próg."
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when battery charge drops below a threshold"
|
||||
msgstr ""
|
||||
msgstr "Uruchamia się, gdy poziom baterii spadnie poniżej wybranej wartości"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
@@ -1569,7 +1569,7 @@ msgstr "Wyzwalane, gdy użycie GPU przekroczy próg"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when memory usage exceeds a threshold"
|
||||
msgstr "Wyzwalane, wykorzystanie pamięci przekroczy ustalony próg."
|
||||
msgstr "Wyzwalane, wykorzystanie pamięci przekroczy ustalony próg"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when status switches between up and down"
|
||||
@@ -1690,7 +1690,7 @@ msgstr "Oczekiwanie na wystarczającą liczbę rekordów do wyświetlenia"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
|
||||
msgstr "Chcesz pomóc nam uczynić nasze tłumaczenia jeszcze lepszymi? Sprawdź <0>Crowdin</0> po więcej szczegółów."
|
||||
msgstr "Chcesz pomóc ulepszyć nasze tłumaczenie? Sprawdź <0>Crowdin</0> po szczegóły."
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Wants"
|
||||
@@ -1722,7 +1722,7 @@ msgstr "Polecenie Windows"
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Write"
|
||||
msgstr "Napisz"
|
||||
msgstr "Zapis"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "YAML Config"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Tak"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pt\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -42,7 +42,7 @@ 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 {# thread} other {# threads}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "1 hour"
|
||||
@@ -230,7 +230,7 @@ msgstr "Largura de Banda"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -284,7 +284,7 @@ msgstr "Bytes (KB/s, MB/s, GB/s)"
|
||||
|
||||
#: src/components/charts/mem-chart.tsx
|
||||
msgid "Cache / Buffers"
|
||||
msgstr ""
|
||||
msgstr "Cache / Buffers"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Can reload"
|
||||
@@ -813,7 +813,7 @@ msgstr "Comando Homebrew"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Host / IP"
|
||||
msgstr ""
|
||||
msgstr "Host / IP"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -1430,7 +1430,7 @@ msgstr "Tarefas"
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Temp"
|
||||
msgstr ""
|
||||
msgstr "Temp"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1493,7 +1493,7 @@ msgstr "Alternar tema"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Sim"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "As configurações do seu usuário foram atualizadas."
|
||||
|
||||
|
||||
1744
internal/site/src/locales/ro/ro.po
Normal file
1744
internal/site/src/locales/ro/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ru\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:17\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Пропускная способность"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr "Батарея"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Да"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваши настройки пользователя были обновлены."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: sl\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
||||
@@ -142,7 +142,7 @@ msgstr "Po"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
msgstr "Agent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
@@ -230,7 +230,7 @@ msgstr "Pasovna širina"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -456,7 +456,7 @@ msgstr "Kopiraj YAML"
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
@@ -706,7 +706,7 @@ msgstr "Izvozi trenutne nastavitve sistema."
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Fahrenheit (°F)"
|
||||
msgstr "Fahrenheit (°F)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Failed"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Da"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: sr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-08 18:22\n"
|
||||
"PO-Revision-Date: 2026-02-03 15:27\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Serbian (Cyrillic)\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"
|
||||
@@ -157,6 +157,7 @@ msgstr "Упозорења"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/containers.tsx
|
||||
msgid "All Containers"
|
||||
msgstr "Сви контејнери"
|
||||
@@ -230,7 +231,7 @@ msgstr "Пропусни опсег"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr "Бат"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -837,6 +838,7 @@ msgstr "Неактивно"
|
||||
msgid "Invalid email address."
|
||||
msgstr "Неважећа имејл адреса."
|
||||
|
||||
#: src/components/lang-toggle.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Језик"
|
||||
@@ -1335,6 +1337,7 @@ msgstr "Подесите процентуалне прагове за боје
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Settings"
|
||||
@@ -1486,6 +1489,7 @@ msgstr "На е-пошту(е)"
|
||||
msgid "Toggle grid"
|
||||
msgstr "Укључи/искључи мрежу"
|
||||
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/mode-toggle.tsx
|
||||
msgid "Toggle theme"
|
||||
msgstr "Промени тему"
|
||||
@@ -1741,3 +1745,4 @@ msgstr "Да"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваша корисничка подешавања су ажурирана."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: sv\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-05 20:24\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Bandbredd"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1119,7 +1119,7 @@ msgstr "Procentandel av tid spenderad i varje tillstånd"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Permanent"
|
||||
msgstr "Permanent"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Persistence"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dina användarinställningar har uppdaterats."
|
||||
|
||||
|
||||
1744
internal/site/src/locales/th/th.po
Normal file
1744
internal/site/src/locales/th/th.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: tr\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Evet"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Kullanıcı ayarlarınız güncellendi."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: uk\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||
@@ -230,7 +230,7 @@ msgstr "Пропускна здатність"
|
||||
#. Battery label in systems table header
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Bat"
|
||||
msgstr "Bat"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
#: src/lib/alerts.ts
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Так"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваші налаштування користувача були оновлені."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: vi\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Vietnamese\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -456,7 +456,7 @@ msgstr "Sao chép YAML"
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/system/cpu-sheet.tsx
|
||||
msgid "CPU Cores"
|
||||
@@ -626,7 +626,7 @@ msgstr "Chỉnh sửa {foo}"
|
||||
#: src/components/login/forgot-pass-form.tsx
|
||||
#: src/components/login/otp-forms.tsx
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
msgid "Email notifications"
|
||||
@@ -1493,7 +1493,7 @@ msgstr "Chuyển đổi chủ đề"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
@@ -1741,3 +1741,4 @@ msgstr "Có"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Cài đặt người dùng của bạn đã được cập nhật."
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: zh\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -113,7 +113,7 @@ msgstr "添加 {foo}"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add <0>System</0>"
|
||||
msgstr "<0>添加客户端</0>"
|
||||
msgstr "添加<0>客户端</0>"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Add system"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "是"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "您的用户设置已更新。"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: zh\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-02 23:18\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Traditional, Hong Kong\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -1741,3 +1741,4 @@ msgstr "是"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "您的用戶設置已更新。"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: zh\n"
|
||||
"Project-Id-Version: beszel\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2026-01-10 17:34\n"
|
||||
"PO-Revision-Date: 2026-01-31 21:16\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Traditional\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
@@ -142,7 +142,7 @@ msgstr "之後"
|
||||
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Agent"
|
||||
msgstr "代理"
|
||||
msgstr "Agent"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
@@ -442,11 +442,11 @@ msgstr "複製文字"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
|
||||
msgstr "複製下面的代理程式安裝指令,或使用<0>通用令牌</0>自動註冊代理程式。"
|
||||
msgstr "以下方指令安裝 Agent ,或使用<0>通用 Token</0>進行自動註冊。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "複製下面的代理程式<0>docker-compose.yml</0>內容,或使用<1>通用令牌</1>自動註冊代理程式。"
|
||||
msgstr "以下方<0>docker-compose.yml</0>執行 Agent,或使用<1>通用 Token</1>進行自動註冊。"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
@@ -534,7 +534,7 @@ msgstr "刪除"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Delete fingerprint"
|
||||
msgstr "刪除指紋"
|
||||
msgstr "刪除 Fingerprint"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Description"
|
||||
@@ -690,7 +690,7 @@ msgstr "結束"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Expires after one hour or on hub restart."
|
||||
msgstr "一小時後或重新啟動集線器時過期。"
|
||||
msgstr "在一個小時後或者重新啟動 Hub 時過期。"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Export"
|
||||
@@ -750,7 +750,7 @@ msgstr "篩選..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "指紋"
|
||||
msgstr "Fingerprint"
|
||||
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Firmware"
|
||||
@@ -1250,7 +1250,7 @@ msgstr "Root"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Rotate token"
|
||||
msgstr "輪替令牌"
|
||||
msgstr "重設 Token"
|
||||
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Rows per page"
|
||||
@@ -1283,7 +1283,7 @@ msgstr "儲存系統"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Saved in the database and does not expire until you disable it."
|
||||
msgstr "保存在資料庫中,在您停用之前不會過期。"
|
||||
msgstr "儲存在資料庫中,在您停用之前不會過期。"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Schedule"
|
||||
@@ -1493,21 +1493,21 @@ msgstr "切換主題"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "令牌"
|
||||
msgstr "Token"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens & Fingerprints"
|
||||
msgstr "令牌和指紋"
|
||||
msgstr "Token & Fingerprint"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
|
||||
msgstr "令牌允許代理程式連線和註冊。指紋是每個系統的唯一穩定識別符,在首次連線時產生。"
|
||||
msgstr "Token 用於 Agent 的連線和註冊。Fingerprint 則是每個系統唯一的穩定識別碼,於初次連線時設定。"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "令牌和指紋被用於驗證到 Hub 的 WebSocket 連線。"
|
||||
msgstr "Token 與 Fingerprint 用於驗證連往 Hub 的 WebSocket 連線。"
|
||||
|
||||
#: src/components/ui/chart.tsx
|
||||
#: src/components/ui/chart.tsx
|
||||
@@ -1597,7 +1597,7 @@ msgstr "單位偏好"
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Universal token"
|
||||
msgstr "通用令牌"
|
||||
msgstr "通用 Token"
|
||||
|
||||
#. Context: Battery state
|
||||
#: src/lib/i18n.ts
|
||||
@@ -1710,7 +1710,7 @@ msgstr "Webhook / 推送通知"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "When enabled, this token allows agents to self-register without prior system creation."
|
||||
msgstr "啟用後,此令牌允許代理無需事先建立系統即可自行註冊。"
|
||||
msgstr "啟用後,此 Token 可讓 Agent 自行註冊,無需預先在系統中建立項目。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -1741,3 +1741,4 @@ msgstr "是"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "已更新您的使用者設定"
|
||||
|
||||
|
||||
6
internal/site/src/types.d.ts
vendored
6
internal/site/src/types.d.ts
vendored
@@ -215,9 +215,11 @@ interface ContainerStats {
|
||||
/** memory used (gb) */
|
||||
m: number
|
||||
// network sent (mb)
|
||||
ns: number
|
||||
ns?: number
|
||||
// network received (mb)
|
||||
nr: number
|
||||
nr?: number
|
||||
/** bandwidth bytes [sent, recv] */
|
||||
b?: [number, number]
|
||||
}
|
||||
|
||||
export interface SystemStatsRecord extends RecordModel {
|
||||
|
||||
@@ -9,13 +9,6 @@ SERVICE_USER=beszel
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
# This would normally be in the config control file, however this is currently
|
||||
# broken in goreleaser. Temporarily do it here.
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
db_version 2.0
|
||||
db_input high beszel-agent/key || true
|
||||
db_go
|
||||
|
||||
# Create group and user
|
||||
if ! getent group "$SERVICE_USER" >/dev/null; then
|
||||
echo "Creating $SERVICE_USER group"
|
||||
|
||||
@@ -12,6 +12,24 @@ is_freebsd() {
|
||||
[ "$(uname -s)" = "FreeBSD" ]
|
||||
}
|
||||
|
||||
is_glibc() {
|
||||
# Prefer glibc-enabled agent (NVML via purego) on linux/amd64 glibc systems.
|
||||
# Check common dynamic loader paths first (fast + reliable).
|
||||
for p in \
|
||||
/lib64/ld-linux-x86-64.so.2 \
|
||||
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 \
|
||||
/lib/ld-linux-x86-64.so.2; do
|
||||
[ -e "$p" ] && return 0
|
||||
done
|
||||
|
||||
# Fallback to ldd output if available.
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
ldd --version 2>&1 | grep -qiE 'gnu libc|glibc' && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
# If SELinux is enabled, set the context of the binary
|
||||
set_selinux_context() {
|
||||
@@ -355,6 +373,20 @@ else
|
||||
BIN_PATH="/opt/beszel-agent/beszel-agent"
|
||||
fi
|
||||
|
||||
# Stop existing service if it exists (for upgrades)
|
||||
if [ -f "$BIN_PATH" ]; then
|
||||
echo "Existing installation detected. Stopping service for upgrade..."
|
||||
if is_alpine; then
|
||||
rc-service beszel-agent stop 2>/dev/null || true
|
||||
elif is_openwrt; then
|
||||
/etc/init.d/beszel-agent stop 2>/dev/null || true
|
||||
elif is_freebsd; then
|
||||
service beszel-agent stop 2>/dev/null || true
|
||||
else
|
||||
systemctl stop beszel-agent.service 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Uninstall process
|
||||
if [ "$UNINSTALL" = true ]; then
|
||||
# Clean up SELinux contexts before removing files
|
||||
@@ -489,10 +521,14 @@ else
|
||||
echo "Warning: Please ensure 'tar' and 'curl' and 'sha256sum (coreutils)' are installed."
|
||||
fi
|
||||
|
||||
# If no SSH key is provided, ask for the SSH key interactively
|
||||
# If no SSH key is provided, ask for the SSH key interactively (skip if upgrading)
|
||||
if [ -z "$KEY" ]; then
|
||||
printf "Enter your SSH key: "
|
||||
read KEY
|
||||
if [ -f "$BIN_PATH" ]; then
|
||||
echo "Upgrading existing installation. Using existing service configuration."
|
||||
else
|
||||
printf "Enter your SSH key: "
|
||||
read KEY
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove newlines from KEY
|
||||
@@ -522,7 +558,7 @@ if is_alpine; then
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
if getent group docker; then
|
||||
echo "Adding beszel to docker group"
|
||||
usermod -aG docker beszel
|
||||
addgroup beszel docker
|
||||
fi
|
||||
|
||||
elif is_openwrt; then
|
||||
@@ -598,6 +634,9 @@ echo "Downloading and installing the agent..."
|
||||
OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
|
||||
ARCH=$(detect_architecture)
|
||||
FILE_NAME="beszel-agent_${OS}_${ARCH}.tar.gz"
|
||||
if [ "$OS" = "linux" ] && [ "$ARCH" = "amd64" ] && is_glibc; then
|
||||
FILE_NAME="beszel-agent_${OS}_${ARCH}_glibc.tar.gz"
|
||||
fi
|
||||
|
||||
# Determine version to install
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
@@ -646,6 +685,11 @@ if ! tar -xzf "$FILE_NAME" beszel-agent; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$BIN_PATH" ]; then
|
||||
echo "Backing up existing binary..."
|
||||
cp "$BIN_PATH" "$BIN_PATH.bak"
|
||||
fi
|
||||
|
||||
mv beszel-agent "$BIN_PATH"
|
||||
chown beszel:beszel "$BIN_PATH"
|
||||
chmod 755 "$BIN_PATH"
|
||||
@@ -674,8 +718,9 @@ detect_nvidia_devices() {
|
||||
|
||||
# Modify service installation part, add Alpine check before systemd service creation
|
||||
if is_alpine; then
|
||||
echo "Creating OpenRC service for Alpine Linux..."
|
||||
cat >/etc/init.d/beszel-agent <<EOF
|
||||
if [ ! -f /etc/init.d/beszel-agent ]; then
|
||||
echo "Creating OpenRC service for Alpine Linux..."
|
||||
cat >/etc/init.d/beszel-agent <<EOF
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name="beszel-agent"
|
||||
@@ -701,9 +746,11 @@ depend() {
|
||||
after firewall
|
||||
}
|
||||
EOF
|
||||
|
||||
chmod +x /etc/init.d/beszel-agent
|
||||
rc-update add beszel-agent default
|
||||
chmod +x /etc/init.d/beszel-agent
|
||||
rc-update add beszel-agent default
|
||||
else
|
||||
echo "Alpine OpenRC service file already exists. Skipping creation."
|
||||
fi
|
||||
|
||||
# Create log files with proper permissions
|
||||
touch /var/log/beszel-agent.log /var/log/beszel-agent.err
|
||||
@@ -750,8 +797,9 @@ EOF
|
||||
fi
|
||||
|
||||
elif is_openwrt; then
|
||||
echo "Creating procd init script service for OpenWRT..."
|
||||
cat >/etc/init.d/beszel-agent <<EOF
|
||||
if [ ! -f /etc/init.d/beszel-agent ]; then
|
||||
echo "Creating procd init script service for OpenWRT..."
|
||||
cat >/etc/init.d/beszel-agent <<EOF
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
@@ -779,10 +827,12 @@ update() {
|
||||
}
|
||||
|
||||
EOF
|
||||
|
||||
# Enable the service
|
||||
chmod +x /etc/init.d/beszel-agent
|
||||
/etc/init.d/beszel-agent enable
|
||||
# Enable the service
|
||||
chmod +x /etc/init.d/beszel-agent
|
||||
/etc/init.d/beszel-agent enable
|
||||
else
|
||||
echo "OpenWRT init script already exists. Skipping creation."
|
||||
fi
|
||||
|
||||
# Start the service
|
||||
/etc/init.d/beszel-agent restart
|
||||
@@ -820,25 +870,33 @@ EOF
|
||||
fi
|
||||
|
||||
elif is_freebsd; then
|
||||
echo "Creating FreeBSD rc service..."
|
||||
echo "Checking for existing FreeBSD service configuration..."
|
||||
|
||||
# Create environment configuration file with proper permissions
|
||||
echo "Creating environment configuration file..."
|
||||
cat >"$AGENT_DIR/env" <<EOF
|
||||
# Create environment configuration file with proper permissions if it doesn't exist
|
||||
if [ ! -f "$AGENT_DIR/env" ]; then
|
||||
echo "Creating environment configuration file..."
|
||||
cat >"$AGENT_DIR/env" <<EOF
|
||||
LISTEN=$PORT
|
||||
KEY="$KEY"
|
||||
TOKEN=$TOKEN
|
||||
HUB_URL=$HUB_URL
|
||||
EOF
|
||||
chmod 640 "$AGENT_DIR/env"
|
||||
chown root:beszel "$AGENT_DIR/env"
|
||||
chmod 640 "$AGENT_DIR/env"
|
||||
chown root:beszel "$AGENT_DIR/env"
|
||||
else
|
||||
echo "FreeBSD environment file already exists. Skipping creation."
|
||||
fi
|
||||
|
||||
# Create the rc service file
|
||||
generate_freebsd_rc_service > /usr/local/etc/rc.d/beszel-agent
|
||||
# Create the rc service file if it doesn't exist
|
||||
if [ ! -f /usr/local/etc/rc.d/beszel-agent ]; then
|
||||
echo "Creating FreeBSD rc service..."
|
||||
generate_freebsd_rc_service > /usr/local/etc/rc.d/beszel-agent
|
||||
# Set proper permissions for the rc script
|
||||
chmod 755 /usr/local/etc/rc.d/beszel-agent
|
||||
else
|
||||
echo "FreeBSD rc service file already exists. Skipping creation."
|
||||
fi
|
||||
|
||||
# Set proper permissions for the rc script
|
||||
chmod 755 /usr/local/etc/rc.d/beszel-agent
|
||||
|
||||
# Enable and start the service
|
||||
echo "Enabling and starting the agent service..."
|
||||
sysrc beszel_agent_enable="YES"
|
||||
@@ -884,12 +942,13 @@ EOF
|
||||
|
||||
else
|
||||
# Original systemd service installation code
|
||||
echo "Creating the systemd service for the agent..."
|
||||
if [ ! -f /etc/systemd/system/beszel-agent.service ]; then
|
||||
echo "Creating the systemd service for the agent..."
|
||||
|
||||
# Detect NVIDIA devices and grant device permissions
|
||||
NVIDIA_DEVICES=$(detect_nvidia_devices)
|
||||
# Detect NVIDIA devices and grant device permissions
|
||||
NVIDIA_DEVICES=$(detect_nvidia_devices)
|
||||
|
||||
cat >/etc/systemd/system/beszel-agent.service <<EOF
|
||||
cat >/etc/systemd/system/beszel-agent.service <<EOF
|
||||
[Unit]
|
||||
Description=Beszel Agent Service
|
||||
Wants=network-online.target
|
||||
@@ -923,12 +982,15 @@ $(if [ -n "$NVIDIA_DEVICES" ]; then printf "%b" "# NVIDIA device permissions\n${
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
else
|
||||
echo "Systemd service file already exists. Skipping creation."
|
||||
fi
|
||||
|
||||
# Load and start the service
|
||||
printf "\nLoading and starting the agent service...\n"
|
||||
systemctl daemon-reload
|
||||
systemctl enable beszel-agent.service
|
||||
systemctl start beszel-agent.service
|
||||
systemctl restart beszel-agent.service
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user