mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
4 Commits
031abbfcb3
...
23c4958145
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c4958145 | ||
|
|
edb2edc12c | ||
|
|
648a979a81 | ||
|
|
988de6de7b |
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:
|
body:
|
||||||
- type: markdown
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
label: Welcome!
|
||||||
### Before opening a discussion:
|
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).
|
Please note:
|
||||||
- Search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
- 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
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Problem Description
|
||||||
description: A clear and concise description of the issue or question. If applicable, add screenshots to help explain your problem.
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: system
|
id: system
|
||||||
attributes:
|
attributes:
|
||||||
@@ -21,13 +56,15 @@ body:
|
|||||||
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
|
||||||
id: version
|
# - type: input
|
||||||
attributes:
|
# id: version
|
||||||
label: Beszel version
|
# attributes:
|
||||||
placeholder: 0.9.1
|
# label: Beszel version
|
||||||
validations:
|
# placeholder: 0.9.1
|
||||||
required: true
|
# validations:
|
||||||
|
# required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: install-method
|
id: install-method
|
||||||
attributes:
|
attributes:
|
||||||
@@ -41,18 +78,21 @@ body:
|
|||||||
- Other (please describe above)
|
- Other (please describe above)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: config
|
id: config
|
||||||
attributes:
|
attributes:
|
||||||
label: Configuration
|
label: Configuration
|
||||||
description: Please provide any relevant service configuration
|
description: Please provide any relevant service configuration
|
||||||
render: yaml
|
render: yaml
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: hub-logs
|
id: hub-logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Hub Logs
|
label: Hub Logs
|
||||||
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
||||||
render: json
|
render: json
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: agent-logs
|
id: agent-logs
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
103
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,30 @@
|
|||||||
name: 🐛 Bug report
|
name: 🐛 Bug report
|
||||||
description: Report a new bug or issue.
|
description: Use this template to report a bug or issue.
|
||||||
title: '[Bug]: '
|
title: '[Bug]: '
|
||||||
labels: ['bug', "needs confirmation"]
|
labels: ['bug']
|
||||||
body:
|
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
|
- type: dropdown
|
||||||
id: component
|
id: component
|
||||||
attributes:
|
attributes:
|
||||||
@@ -12,81 +34,53 @@ body:
|
|||||||
- Hub
|
- Hub
|
||||||
- Agent
|
- Agent
|
||||||
- Hub & Agent
|
- Hub & Agent
|
||||||
|
default: 0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Problem Description
|
||||||
description: Explain the issue you experienced clearly and concisely.
|
description: |
|
||||||
placeholder: I went to the coffee pot and it was empty.
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: expected-behavior
|
id: expected-behavior
|
||||||
attributes:
|
attributes:
|
||||||
label: Expected Behavior
|
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.
|
placeholder: When I got to the coffee pot, it should have been full.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps-to-reproduce
|
id: steps-to-reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce
|
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: |
|
placeholder: |
|
||||||
1. Go to the coffee pot.
|
1. Go to the coffee pot.
|
||||||
2. Make more coffee.
|
2. Make more coffee.
|
||||||
3. Pour it into a cup.
|
3. Pour it into a cup.
|
||||||
|
4. Observe that the cup is empty instead of full.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: input
|
||||||
id: system
|
id: system
|
||||||
attributes:
|
attributes:
|
||||||
@@ -94,6 +88,7 @@ body:
|
|||||||
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
@@ -101,6 +96,7 @@ body:
|
|||||||
placeholder: 0.9.1
|
placeholder: 0.9.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: install-method
|
id: install-method
|
||||||
attributes:
|
attributes:
|
||||||
@@ -114,18 +110,21 @@ body:
|
|||||||
- Other (please describe above)
|
- Other (please describe above)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: config
|
id: config
|
||||||
attributes:
|
attributes:
|
||||||
label: Configuration
|
label: Configuration
|
||||||
description: Please provide any relevant service configuration
|
description: Please provide any relevant service configuration
|
||||||
render: yaml
|
render: yaml
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: hub-logs
|
id: hub-logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Hub Logs
|
label: Hub Logs
|
||||||
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
||||||
render: json
|
render: json
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: agent-logs
|
id: agent-logs
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
|
- name: 🗣️ Translations
|
||||||
|
url: https://crowdin.com/project/beszel
|
||||||
|
about: Please report translation issues and request new translations here.
|
||||||
- name: 💬 Support and questions
|
- name: 💬 Support and questions
|
||||||
url: https://github.com/henrygd/beszel/discussions
|
url: https://github.com/henrygd/beszel/discussions
|
||||||
about: Ask and answer questions here.
|
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
|
name: 🚀 Feature request
|
||||||
description: Request a new feature or change.
|
description: Request a new feature or change.
|
||||||
title: "[Feature]: "
|
title: "[Feature]: "
|
||||||
labels: ["enhancement", "needs review"]
|
labels: ["enhancement"]
|
||||||
body:
|
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
|
- type: dropdown
|
||||||
id: component
|
id: component
|
||||||
attributes:
|
attributes:
|
||||||
@@ -12,16 +29,25 @@ body:
|
|||||||
- Hub
|
- Hub
|
||||||
- Agent
|
- Agent
|
||||||
- Hub & Agent
|
- Hub & Agent
|
||||||
|
default: 0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: textarea
|
||||||
|
id: description
|
||||||
attributes:
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: motivation
|
id: motivation
|
||||||
attributes:
|
attributes:
|
||||||
@@ -29,48 +55,3 @@ body:
|
|||||||
description: Why do you want this feature? What problem does it solve?
|
description: Why do you want this feature? What problem does it solve?
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
# Labels
|
||||||
stale-issue-label: 'stale'
|
stale-issue-label: 'stale'
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
only-issue-labels: 'awaiting-requester'
|
any-of-labels: 'awaiting-requester'
|
||||||
|
exempt-issue-labels: 'enhancement'
|
||||||
|
|
||||||
# Exemptions
|
# Exemptions
|
||||||
exempt-assignees: true
|
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
130
agent/smart.go
130
agent/smart.go
@@ -54,6 +54,12 @@ type DeviceInfo struct {
|
|||||||
parserType string
|
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
|
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
||||||
|
|
||||||
// Refresh updates SMART data for all known devices
|
// Refresh updates SMART data for all known devices
|
||||||
@@ -165,7 +171,7 @@ func (sm *SmartManager) ScanDevices(force bool) error {
|
|||||||
configuredDevices = parsedDevices
|
configuredDevices = parsedDevices
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, sm.binPath, "--scan", "-j")
|
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) {
|
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))
|
devices := make([]*DeviceInfo, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
entry = strings.TrimSpace(entry)
|
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
|
// parseSmartOutput attempts each SMART parser, optionally detecting the type when
|
||||||
// it is not provided, and updates the device info when a parser succeeds.
|
// it is not provided, and updates the device info when a parser succeeds.
|
||||||
func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte) bool {
|
func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte) bool {
|
||||||
@@ -569,6 +586,28 @@ func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo
|
|||||||
return existing
|
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
|
// preserveVerifiedType copies the verified type/parser metadata from an existing
|
||||||
// device record so that subsequent scans/config updates never downgrade a
|
// device record so that subsequent scans/config updates never downgrade a
|
||||||
// previously verified device.
|
// previously verified device.
|
||||||
@@ -581,69 +620,90 @@ func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo
|
|||||||
target.parserType = prev.parserType
|
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 {
|
for _, dev := range existing {
|
||||||
if dev == nil || dev.Name == "" {
|
if dev == nil || dev.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
existingIndex[dev.Name] = dev
|
existingIndex[makeDeviceKey(dev.Name, dev.Type)] = dev
|
||||||
}
|
}
|
||||||
|
existingByName := buildUniqueNameIndex(existing)
|
||||||
|
|
||||||
finalDevices := make([]*DeviceInfo, 0, len(scanned)+len(configured))
|
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,
|
// Start with the newly scanned devices so we always surface fresh metadata,
|
||||||
// but ensure we retain any previously verified parser assignment.
|
// but ensure we retain any previously verified parser assignment.
|
||||||
for _, dev := range scanned {
|
for _, scannedDevice := range scanned {
|
||||||
if dev == nil || dev.Name == "" {
|
if scannedDevice == nil || scannedDevice.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work on a copy so we can safely adjust metadata without mutating the
|
// Work on a copy so we can safely adjust metadata without mutating the
|
||||||
// input slices that may be reused elsewhere.
|
// input slices that may be reused elsewhere.
|
||||||
copyDev := *dev
|
copyDev := *scannedDevice
|
||||||
if prev := existingIndex[copyDev.Name]; prev != nil {
|
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)
|
preserveVerifiedType(©Dev, prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalDevices = append(finalDevices, ©Dev)
|
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
|
// Merge configured devices on top so users can override scan results (except
|
||||||
// for verified type information).
|
// for verified type information).
|
||||||
for _, dev := range configured {
|
for _, configuredDevice := range configured {
|
||||||
if dev == nil || dev.Name == "" {
|
if configuredDevice == nil || configuredDevice.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingDev, ok := deviceIndex[dev.Name]; ok {
|
key := makeDeviceKey(configuredDevice.Name, configuredDevice.Type)
|
||||||
// Only update the type if it has not been verified yet; otherwise we
|
if existingDev, ok := deviceIndex[key]; ok {
|
||||||
// keep the existing verified metadata intact.
|
applyConfiguredMetadata(existingDev, configuredDevice)
|
||||||
if dev.Type != "" && !existingDev.typeVerified {
|
continue
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {
|
||||||
|
applyConfiguredMetadata(existingDev, configuredDevice)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
copyDev := *dev
|
copyDev := *configuredDevice
|
||||||
if prev := existingIndex[copyDev.Name]; prev != nil {
|
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)
|
preserveVerifiedType(©Dev, prev)
|
||||||
} else if copyDev.Type != "" {
|
} else if copyDev.Type != "" {
|
||||||
copyDev.parserType = normalizeParserType(copyDev.Type)
|
copyDev.parserType = normalizeParserType(copyDev.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalDevices = append(finalDevices, ©Dev)
|
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
|
return finalDevices
|
||||||
@@ -661,12 +721,14 @@ func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {
|
|||||||
return
|
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 {
|
for _, device := range devices {
|
||||||
if device == nil || device.Name == "" {
|
if device == nil || device.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
validNames[device.Name] = struct{}{}
|
validKeys[makeDeviceKey(device.Name, device.Type)] = struct{}{}
|
||||||
|
nameCounts[device.Name]++
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, data := range sm.SmartDataMap {
|
for key, data := range sm.SmartDataMap {
|
||||||
@@ -675,7 +737,11 @@ func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {
|
|||||||
continue
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,24 @@ func TestDevicesSnapshotReturnsCopy(t *testing.T) {
|
|||||||
assert.Len(t, snapshot, 2)
|
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) {
|
func TestScanDevicesWithEnvOverride(t *testing.T) {
|
||||||
t.Setenv("SMART_DEVICES", "/dev/sda:sat, /dev/nvme0:nvme")
|
t.Setenv("SMART_DEVICES", "/dev/sda:sat, /dev/nvme0:nvme")
|
||||||
|
|
||||||
@@ -442,6 +460,88 @@ func TestMergeDeviceListsUpdatesTypeWhenUnverified(t *testing.T) {
|
|||||||
assert.Equal(t, "", device.parserType)
|
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) {
|
func TestParseSmartOutputMarksVerified(t *testing.T) {
|
||||||
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
||||||
data, err := os.ReadFile(fixturePath)
|
data, err := os.ReadFile(fixturePath)
|
||||||
|
|||||||
Reference in New Issue
Block a user