Showing preview only (3,659K chars total). Download the full file or copy to clipboard to get everything.
Repository: henrygd/beszel
Branch: main
Commit: adbfe7cfb7df
Files: 357
Total size: 3.4 MB
Directory structure:
gitextract_xj546sdv/
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── DISCUSSION_TEMPLATE/
│ │ ├── ideas.yml
│ │ └── support.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── funding.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── docker-images.yml
│ ├── inactivity-actions.yml
│ ├── release.yml
│ └── vulncheck.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── SECURITY.md
├── agent/
│ ├── agent.go
│ ├── agent_cache.go
│ ├── agent_cache_test.go
│ ├── agent_test_helpers.go
│ ├── battery/
│ │ ├── battery.go
│ │ └── battery_freebsd.go
│ ├── client.go
│ ├── client_test.go
│ ├── connection_manager.go
│ ├── connection_manager_test.go
│ ├── cpu.go
│ ├── data_dir.go
│ ├── data_dir_test.go
│ ├── deltatracker/
│ │ ├── deltatracker.go
│ │ └── deltatracker_test.go
│ ├── disk.go
│ ├── disk_test.go
│ ├── docker.go
│ ├── docker_test.go
│ ├── emmc_common.go
│ ├── emmc_common_test.go
│ ├── emmc_linux.go
│ ├── emmc_linux_test.go
│ ├── emmc_stub.go
│ ├── fingerprint.go
│ ├── fingerprint_test.go
│ ├── gpu.go
│ ├── gpu_amd_linux.go
│ ├── gpu_amd_linux_test.go
│ ├── gpu_amd_unsupported.go
│ ├── gpu_darwin.go
│ ├── gpu_darwin_test.go
│ ├── gpu_darwin_unsupported.go
│ ├── gpu_intel.go
│ ├── gpu_nvml.go
│ ├── gpu_nvml_linux.go
│ ├── gpu_nvml_unsupported.go
│ ├── gpu_nvml_windows.go
│ ├── gpu_nvtop.go
│ ├── gpu_test.go
│ ├── handlers.go
│ ├── handlers_test.go
│ ├── health/
│ │ ├── health.go
│ │ └── health_test.go
│ ├── lhm/
│ │ ├── beszel_lhm.cs
│ │ └── beszel_lhm.csproj
│ ├── mdraid_linux.go
│ ├── mdraid_linux_test.go
│ ├── mdraid_stub.go
│ ├── network.go
│ ├── network_test.go
│ ├── response.go
│ ├── sensors.go
│ ├── sensors_default.go
│ ├── sensors_test.go
│ ├── sensors_windows.go
│ ├── server.go
│ ├── server_test.go
│ ├── smart.go
│ ├── smart_nonwindows.go
│ ├── smart_test.go
│ ├── smart_windows.go
│ ├── system.go
│ ├── systemd.go
│ ├── systemd_nonlinux.go
│ ├── systemd_nonlinux_test.go
│ ├── systemd_test.go
│ ├── test-data/
│ │ ├── amdgpu.ids
│ │ ├── container.json
│ │ ├── container2.json
│ │ ├── nvtop.json
│ │ ├── smart/
│ │ │ ├── nvme0.json
│ │ │ ├── scan.json
│ │ │ ├── scsi.json
│ │ │ └── sda.json
│ │ └── system_info.json
│ ├── tools/
│ │ └── fetchsmartctl/
│ │ └── main.go
│ ├── update.go
│ ├── utils/
│ │ ├── utils.go
│ │ └── utils_test.go
│ └── zfs/
│ ├── zfs_freebsd.go
│ ├── zfs_linux.go
│ └── zfs_unsupported.go
├── beszel.go
├── go.mod
├── go.sum
├── i18n.yml
├── internal/
│ ├── alerts/
│ │ ├── alerts.go
│ │ ├── alerts_api.go
│ │ ├── alerts_battery_test.go
│ │ ├── alerts_cache.go
│ │ ├── alerts_cache_test.go
│ │ ├── alerts_disk_test.go
│ │ ├── alerts_history.go
│ │ ├── alerts_quiet_hours_test.go
│ │ ├── alerts_smart.go
│ │ ├── alerts_smart_test.go
│ │ ├── alerts_status.go
│ │ ├── alerts_status_test.go
│ │ ├── alerts_system.go
│ │ ├── alerts_system_test.go
│ │ ├── alerts_test.go
│ │ └── alerts_test_helpers.go
│ ├── cmd/
│ │ ├── agent/
│ │ │ ├── agent.go
│ │ │ └── agent_test.go
│ │ └── hub/
│ │ └── hub.go
│ ├── common/
│ │ ├── common-ssh.go
│ │ └── common-ws.go
│ ├── dockerfile_agent
│ ├── dockerfile_agent_alpine
│ ├── dockerfile_agent_intel
│ ├── dockerfile_agent_nvidia
│ ├── dockerfile_hub
│ ├── entities/
│ │ ├── container/
│ │ │ └── container.go
│ │ ├── smart/
│ │ │ ├── smart.go
│ │ │ └── smart_test.go
│ │ ├── system/
│ │ │ └── system.go
│ │ └── systemd/
│ │ ├── systemd.go
│ │ └── systemd_test.go
│ ├── ghupdate/
│ │ ├── extract.go
│ │ ├── ghupdate.go
│ │ ├── ghupdate_test.go
│ │ ├── release.go
│ │ ├── selinux.go
│ │ └── selinux_test.go
│ ├── hub/
│ │ ├── agent_connect.go
│ │ ├── agent_connect_test.go
│ │ ├── config/
│ │ │ ├── config.go
│ │ │ └── config_test.go
│ │ ├── expirymap/
│ │ │ ├── expirymap.go
│ │ │ └── expirymap_test.go
│ │ ├── heartbeat/
│ │ │ ├── heartbeat.go
│ │ │ └── heartbeat_test.go
│ │ ├── hub.go
│ │ ├── hub_test.go
│ │ ├── hub_test_helpers.go
│ │ ├── server_development.go
│ │ ├── server_production.go
│ │ ├── systems/
│ │ │ ├── system.go
│ │ │ ├── system_manager.go
│ │ │ ├── system_realtime.go
│ │ │ ├── system_smart.go
│ │ │ ├── system_systemd_test.go
│ │ │ ├── system_test.go
│ │ │ ├── systems_production.go
│ │ │ ├── systems_test.go
│ │ │ └── systems_test_helpers.go
│ │ ├── transport/
│ │ │ ├── ssh.go
│ │ │ ├── transport.go
│ │ │ └── websocket.go
│ │ ├── update.go
│ │ └── ws/
│ │ ├── handlers.go
│ │ ├── request_manager.go
│ │ ├── request_manager_test.go
│ │ ├── ws.go
│ │ ├── ws_test.go
│ │ └── ws_test_helpers.go
│ ├── migrations/
│ │ ├── 0_collections_snapshot_0_19_0_dev_1.go
│ │ └── initial-settings.go
│ ├── records/
│ │ ├── records.go
│ │ ├── records_test.go
│ │ └── records_test_helpers.go
│ ├── site/
│ │ ├── .gitignore
│ │ ├── .prettierrc
│ │ ├── biome.json
│ │ ├── components.json
│ │ ├── embed.go
│ │ ├── index.html
│ │ ├── lingui.config.ts
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── static/
│ │ │ └── manifest.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── active-alerts.tsx
│ │ │ │ ├── add-system.tsx
│ │ │ │ ├── alerts/
│ │ │ │ │ ├── alert-button.tsx
│ │ │ │ │ └── alerts-sheet.tsx
│ │ │ │ ├── alerts-history-columns.tsx
│ │ │ │ ├── charts/
│ │ │ │ │ ├── area-chart.tsx
│ │ │ │ │ ├── chart-time-select.tsx
│ │ │ │ │ ├── container-chart.tsx
│ │ │ │ │ ├── disk-chart.tsx
│ │ │ │ │ ├── gpu-power-chart.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── line-chart.tsx
│ │ │ │ │ ├── load-average-chart.tsx
│ │ │ │ │ ├── mem-chart.tsx
│ │ │ │ │ ├── swap-chart.tsx
│ │ │ │ │ └── temperature-chart.tsx
│ │ │ │ ├── command-palette.tsx
│ │ │ │ ├── containers-table/
│ │ │ │ │ ├── containers-table-columns.tsx
│ │ │ │ │ └── containers-table.tsx
│ │ │ │ ├── copy-to-clipboard.tsx
│ │ │ │ ├── footer-repo-link.tsx
│ │ │ │ ├── install-dropdowns.tsx
│ │ │ │ ├── lang-toggle.tsx
│ │ │ │ ├── login/
│ │ │ │ │ ├── auth-form.tsx
│ │ │ │ │ ├── forgot-pass-form.tsx
│ │ │ │ │ ├── login.tsx
│ │ │ │ │ └── otp-forms.tsx
│ │ │ │ ├── logo.tsx
│ │ │ │ ├── mode-toggle.tsx
│ │ │ │ ├── navbar.tsx
│ │ │ │ ├── router.tsx
│ │ │ │ ├── routes/
│ │ │ │ │ ├── containers.tsx
│ │ │ │ │ ├── home.tsx
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── alerts-history-data-table.tsx
│ │ │ │ │ │ ├── config-yaml.tsx
│ │ │ │ │ │ ├── general.tsx
│ │ │ │ │ │ ├── heartbeat.tsx
│ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ ├── notifications.tsx
│ │ │ │ │ │ ├── quiet-hours.tsx
│ │ │ │ │ │ ├── sidebar-nav.tsx
│ │ │ │ │ │ └── tokens-fingerprints.tsx
│ │ │ │ │ ├── smart.tsx
│ │ │ │ │ ├── system/
│ │ │ │ │ │ ├── cpu-sheet.tsx
│ │ │ │ │ │ ├── info-bar.tsx
│ │ │ │ │ │ ├── network-sheet.tsx
│ │ │ │ │ │ └── smart-table.tsx
│ │ │ │ │ └── system.tsx
│ │ │ │ ├── spinner.tsx
│ │ │ │ ├── systemd-table/
│ │ │ │ │ ├── systemd-table-columns.tsx
│ │ │ │ │ └── systemd-table.tsx
│ │ │ │ ├── systems-table/
│ │ │ │ │ ├── systems-table-columns.tsx
│ │ │ │ │ └── systems-table.tsx
│ │ │ │ ├── theme-provider.tsx
│ │ │ │ └── ui/
│ │ │ │ ├── alert-dialog.tsx
│ │ │ │ ├── alert.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── chart.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── icons.tsx
│ │ │ │ ├── input-copy.tsx
│ │ │ │ ├── input-tags.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── otp.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── slider.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── toast.tsx
│ │ │ │ ├── toaster.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ └── use-toast.ts
│ │ │ ├── index.css
│ │ │ ├── lib/
│ │ │ │ ├── alerts.ts
│ │ │ │ ├── api.ts
│ │ │ │ ├── enums.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── languages.ts
│ │ │ │ ├── shiki.ts
│ │ │ │ ├── stores.ts
│ │ │ │ ├── systemsManager.ts
│ │ │ │ ├── time.ts
│ │ │ │ ├── use-intersection-observer.ts
│ │ │ │ └── utils.ts
│ │ │ ├── locales/
│ │ │ │ ├── ar/
│ │ │ │ │ └── ar.po
│ │ │ │ ├── bg/
│ │ │ │ │ └── bg.po
│ │ │ │ ├── cs/
│ │ │ │ │ └── cs.po
│ │ │ │ ├── da/
│ │ │ │ │ └── da.po
│ │ │ │ ├── de/
│ │ │ │ │ └── de.po
│ │ │ │ ├── en/
│ │ │ │ │ └── en.po
│ │ │ │ ├── es/
│ │ │ │ │ └── es.po
│ │ │ │ ├── fa/
│ │ │ │ │ └── fa.po
│ │ │ │ ├── fr/
│ │ │ │ │ └── fr.po
│ │ │ │ ├── he/
│ │ │ │ │ └── he.po
│ │ │ │ ├── hr/
│ │ │ │ │ └── hr.po
│ │ │ │ ├── hu/
│ │ │ │ │ └── hu.po
│ │ │ │ ├── id/
│ │ │ │ │ └── id.po
│ │ │ │ ├── it/
│ │ │ │ │ └── it.po
│ │ │ │ ├── ja/
│ │ │ │ │ └── ja.po
│ │ │ │ ├── ko/
│ │ │ │ │ └── ko.po
│ │ │ │ ├── nl/
│ │ │ │ │ └── nl.po
│ │ │ │ ├── no/
│ │ │ │ │ └── no.po
│ │ │ │ ├── pl/
│ │ │ │ │ └── pl.po
│ │ │ │ ├── pt/
│ │ │ │ │ └── pt.po
│ │ │ │ ├── ru/
│ │ │ │ │ └── ru.po
│ │ │ │ ├── sl/
│ │ │ │ │ └── sl.po
│ │ │ │ ├── sr/
│ │ │ │ │ └── sr.po
│ │ │ │ ├── sv/
│ │ │ │ │ └── sv.po
│ │ │ │ ├── tr/
│ │ │ │ │ └── tr.po
│ │ │ │ ├── uk/
│ │ │ │ │ └── uk.po
│ │ │ │ ├── vi/
│ │ │ │ │ └── vi.po
│ │ │ │ ├── zh/
│ │ │ │ │ └── zh.po
│ │ │ │ ├── zh-CN/
│ │ │ │ │ └── zh-CN.po
│ │ │ │ └── zh-HK/
│ │ │ │ └── zh-HK.po
│ │ │ ├── main.tsx
│ │ │ ├── types.d.ts
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── tests/
│ │ ├── api.go
│ │ └── hub.go
│ └── users/
│ └── users.go
├── readme.md
└── supplemental/
├── CHANGELOG.md
├── debian/
│ ├── beszel-agent.service
│ ├── config.sh
│ ├── copyright
│ ├── lintian-overrides
│ ├── postinstall.sh
│ ├── postrm.sh
│ ├── prerm.sh
│ └── templates
├── docker/
│ ├── agent/
│ │ └── docker-compose.yml
│ ├── hub/
│ │ └── docker-compose.yml
│ └── same-system/
│ └── docker-compose.yml
├── guides/
│ └── systemd.md
├── kubernetes/
│ └── beszel-hub/
│ └── charts/
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates/
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ ├── service.yaml
│ │ ├── tests/
│ │ │ └── test-beszel-hub-endpoint.yaml
│ │ └── volume-claim.yaml
│ └── values.yaml
├── licenses/
│ ├── LibreHardwareMonitor/
│ │ └── LICENSE
│ └── smartmontools/
│ └── LICENSE
└── scripts/
├── install-agent-brew.sh
├── install-agent.ps1
├── install-agent.sh
├── install-hub.sh
├── upgrade-agent-wrapper.ps1
└── upgrade-agent.ps1
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Node.js dependencies
node_modules
internalsite/node_modules
# Go build artifacts and binaries
build
dist
*.exe
beszel-agent
beszel_data*
pb_data
data
temp
# Development and IDE files
.vscode
.idea*
*.swc
__debug_*
# Git and version control
.git
.gitignore
# Documentation and supplemental files
*.md
supplemental
freebsd-port
# Test files (exclude from production builds)
*_test.go
coverage
# Docker files
dockerfile_*
# Temporary files
*.tmp
*.bak
*.log
# OS specific files
.DS_Store
Thumbs.db
# .NET build artifacts
agent/lhm/obj
agent/lhm/bin
================================================
FILE: .gitattributes
================================================
*.tsx linguist-language=Go
================================================
FILE: .github/CODEOWNERS
================================================
# Everything needs to be reviewed by Hank
* @henrygd
================================================
FILE: .github/DISCUSSION_TEMPLATE/ideas.yml
================================================
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
================================================
FILE: .github/DISCUSSION_TEMPLATE/support.yml
================================================
body:
- type: checkboxes
id: terms
attributes:
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:
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: 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:
label: OS / Architecture
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: dropdown
id: install-method
attributes:
label: Installation method
options:
- Docker
- Binary
- Nix
- Unraid
- Coolify
- 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:
label: Agent Logs
description: Please provide any logs from the agent, if relevant. Use `LOG_LEVEL=debug` for more info.
render: shell
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐛 Bug report
description: Use this template to report a bug or issue.
title: '[Bug]: '
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:
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: 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?
**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: |
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: input
id: system
attributes:
label: OS / Architecture
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: dropdown
id: install-method
attributes:
label: Installation method
options:
- Docker
- Binary
- Nix
- Unraid
- Coolify
- 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:
label: Agent Logs
description: Please provide any logs from the agent, if relevant. Use `LOG_LEVEL=debug` for more info.
render: shell
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
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.
- name: ℹ️ View the Common Issues page
url: https://beszel.dev/guide/common-issues
about: Find information about commonly encountered problems.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 🚀 Feature request
description: Request a new feature or change.
title: "[Feature]: "
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:
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: |
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
================================================
FILE: .github/funding.yml
================================================
buy_me_a_coffee: henrygd
================================================
FILE: .github/pull_request_template.md
================================================
## 📃 Description
A short description of the pull request changes should go here and the sections below should list in detail all changes. You can remove the sections you don't need.
## 📖 Documentation
Add a link to the PR for [documentation](https://github.com/henrygd/beszel-docs) changes.
## 🪵 Changelog
### ➕ Added
- one
- two
### ✏️ Changed
- one
- two
### 🔧 Fixed
- one
- two
### 🗑️ Removed
- one
- two
## 📷 Screenshots
If this PR has any UI/UX changes it's strongly suggested you add screenshots here.
================================================
FILE: .github/workflows/docker-images.yml
================================================
name: Make docker images
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
include:
# henrygd/beszel
- image: henrygd/beszel
dockerfile: ./internal/dockerfile_hub
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# henrygd/beszel-agent:alpine
- image: henrygd/beszel-agent
dockerfile: ./internal/dockerfile_agent_alpine
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=alpine
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
# henrygd/beszel-agent-nvidia
- image: henrygd/beszel-agent-nvidia
dockerfile: ./internal/dockerfile_agent_nvidia
platforms: linux/amd64
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# henrygd/beszel-agent-intel
- image: henrygd/beszel-agent-intel
dockerfile: ./internal/dockerfile_agent_intel
platforms: linux/amd64
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# ghcr.io/henrygd/beszel
- image: ghcr.io/${{ github.repository }}/beszel
dockerfile: ./internal/dockerfile_hub
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# ghcr.io/henrygd/beszel-agent
- image: ghcr.io/${{ github.repository }}/beszel-agent
dockerfile: ./internal/dockerfile_agent
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
tags: |
type=raw,value=edge
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# ghcr.io/henrygd/beszel-agent-nvidia
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
dockerfile: ./internal/dockerfile_agent_nvidia
platforms: linux/amd64
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# ghcr.io/henrygd/beszel-agent-intel
- image: ghcr.io/${{ github.repository }}/beszel-agent-intel
dockerfile: ./internal/dockerfile_agent_intel
platforms: linux/amd64
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# ghcr.io/henrygd/beszel-agent:alpine
- image: ghcr.io/${{ github.repository }}/beszel-agent
dockerfile: ./internal/dockerfile_agent_alpine
registry: ghcr.io
username: ${{ github.actor }}
password_secret: GITHUB_TOKEN
tags: |
type=raw,value=alpine
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
# henrygd/beszel-agent (keep at bottom so it gets built after :alpine and gets the latest tag)
- image: henrygd/beszel-agent
dockerfile: ./internal/dockerfile_agent
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --no-save --cwd ./internal/site
- name: Build site
run: bun run --cwd ./internal/site build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker metadata
id: metadata
uses: docker/metadata-action@v5
with:
images: ${{ matrix.image }}
tags: ${{ matrix.tags }}
# https://github.com/docker/login-action
- name: Login to Docker Hub
env:
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
uses: docker/login-action@v3
with:
username: ${{ matrix.username || secrets[matrix.username_secret] }}
password: ${{ secrets[matrix.password_secret] }}
registry: ${{ matrix.registry }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
================================================
FILE: .github/workflows/inactivity-actions.yml
================================================
name: 'Issue and PR Maintenance'
on:
schedule:
- cron: '0 0 * * *' # runs at midnight UTC
workflow_dispatch:
permissions:
actions: write
issues: write
pull-requests: write
jobs:
lock-inactive:
name: Lock Inactive Issues
runs-on: ubuntu-24.04
steps:
- uses: klaasnicolaas/action-inactivity-lock@v1.1.3
id: lock
with:
days-inactive-issues: 14
lock-reason-issues: ""
# Action can not skip PRs, set it to 100 years to cover it.
days-inactive-prs: 36524
lock-reason-prs: ""
close-stale:
name: Close Stale Issues
runs-on: ubuntu-24.04
steps:
- name: Close Stale Issues
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Messaging
stale-issue-message: >
👋 This issue has been automatically marked as stale due to inactivity.
If this issue is still relevant, please comment to keep it open.
Without activity, it will be closed in 7 days.
close-issue-message: >
🔒 This issue has been automatically closed due to prolonged inactivity.
Feel free to open a new issue if you have further questions or concerns.
# Timing
days-before-issue-stale: 14
days-before-issue-close: 7
# Action can not skip PRs, set it to 100 years to cover it.
days-before-pr-stale: 36524
# Max issues to process before early exit. Next run resumes from cache. GH API limit: 5000.
operations-per-run: 1500
# Labels
stale-issue-label: 'stale'
remove-stale-when-updated: true
any-of-labels: 'awaiting-requester'
exempt-issue-labels: 'enhancement'
# Exemptions
exempt-assignees: true
exempt-milestones: true
================================================
FILE: .github/workflows/release.yml
================================================
name: Make release and binaries
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --no-save --cwd ./internal/site
- name: Build site
run: bun run --cwd ./internal/site build
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "^1.22.1"
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
- name: Build .NET LHM executable for Windows sensors
run: |
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj
shell: bash
- name: GoReleaser beszel
uses: goreleaser/goreleaser-action@v6
with:
workdir: ./
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
IS_FORK: ${{ github.repository_owner != 'henrygd' }}
================================================
FILE: .github/workflows/vulncheck.yml
================================================
# https://github.com/minio/minio/blob/master/.github/workflows/vulncheck.yml
name: VulnCheck
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
vulncheck:
name: VulnCheck
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: 1.26.x
# cached: false
- name: Get official govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
shell: bash
- name: Run govulncheck
run: govulncheck -show verbose ./...
shell: bash
================================================
FILE: .gitignore
================================================
.idea.md
pb_data
data
temp
.vscode
beszel-agent
beszel_data
beszel_data*
dist
*.exe
internal/cmd/hub/hub
internal/cmd/agent/agent
agent.test
node_modules
build
*timestamp*
.swc
internal/site/src/locales/**/*.ts
*.bak
__debug_*
agent/lhm/obj
agent/lhm/bin
dockerfile_agent_dev
================================================
FILE: .goreleaser.yml
================================================
version: 2
project_name: beszel
before:
hooks:
- go mod tidy
- go generate -run fetchsmartctl ./agent
builds:
- id: beszel
binary: beszel
main: internal/cmd/hub/hub.go
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
- arm
ignore:
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: freebsd
goarch: arm64
- goos: freebsd
goarch: arm
- id: beszel-agent
binary: beszel-agent
main: internal/cmd/agent/agent.go
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- freebsd
- openbsd
- windows
goarch:
- amd64
- arm64
- arm
- mips64
- riscv64
- mipsle
- mips
- ppc64le
gomips:
- hardfloat
- softfloat
ignore:
- goos: freebsd
goarch: arm
- goos: openbsd
goarch: arm
- goos: linux
goarch: mips64
gomips: softfloat
- goos: linux
goarch: mipsle
gomips: hardfloat
- goos: linux
goarch: mips
gomips: hardfloat
- goos: windows
goarch: arm
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64
- id: beszel-agent-linux-amd64-glibc
binary: beszel-agent
main: internal/cmd/agent/agent.go
env:
- CGO_ENABLED=0
flags:
- -tags=glibc
goos:
- linux
goarch:
- amd64
archives:
- id: beszel-agent
formats: [tar.gz]
ids:
- beszel-agent
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
format_overrides:
- goos: windows
formats: [zip]
- id: beszel-agent-linux-amd64-glibc
formats: [tar.gz]
ids:
- beszel-agent-linux-amd64-glibc
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}_glibc
- id: beszel
formats: [tar.gz]
ids:
- beszel
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
format_overrides:
- goos: windows
formats: [zip]
nfpms:
- id: beszel-agent
package_name: beszel-agent
description: |-
Agent for Beszel
Beszel is a lightweight server monitoring platform that includes Docker
statistics, historical data, and alert functions. It has a friendly web
interface, simple configuration, and is ready to use out of the box.
It supports automatic backup, multi-user, OAuth authentication, and
API access.
maintainer: henrygd <hank@henrygd.me>
section: net
ids:
- beszel-agent
formats:
- deb
contents:
- src: ./supplemental/debian/beszel-agent.service
dst: lib/systemd/system/beszel-agent.service
packager: deb
- src: ./supplemental/debian/copyright
dst: usr/share/doc/beszel-agent/copyright
packager: deb
- src: ./supplemental/debian/lintian-overrides
dst: usr/share/lintian/overrides/beszel-agent
packager: deb
scripts:
postinstall: ./supplemental/debian/postinstall.sh
preremove: ./supplemental/debian/prerm.sh
postremove: ./supplemental/debian/postrm.sh
deb:
predepends:
- adduser
- debconf
scripts:
templates: ./supplemental/debian/templates
config: ./supplemental/debian/config.sh
scoops:
- ids: [beszel-agent]
name: beszel-agent
repository:
owner: henrygd
name: beszel-scoops
homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
# chocolateys:
# - title: Beszel Agent
# ids: [beszel-agent]
# package_source_url: https://github.com/henrygd/beszel-chocolatey
# owners: henrygd
# authors: henrygd
# summary: 'Agent for Beszel, a lightweight server monitoring platform.'
# description: |
# Beszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions.
# It has a friendly web interface, simple configuration, and is ready to use out of the box. It supports automatic backup, multi-user, OAuth authentication, and API access.
# license_url: https://github.com/henrygd/beszel/blob/main/LICENSE
# project_url: https://beszel.dev
# project_source_url: https://github.com/henrygd/beszel
# docs_url: https://beszel.dev/guide/getting-started
# icon_url: https://cdn.jsdelivr.net/gh/selfhst/icons/png/beszel.png
# bug_tracker_url: https://github.com/henrygd/beszel/issues
# copyright: 2025 henrygd
# tags: foss cross-platform admin monitoring
# require_license_acceptance: false
# release_notes: 'https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}'
brews:
- ids: [beszel-agent]
name: beszel-agent
repository:
owner: henrygd
name: homebrew-beszel
homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
extra_install: |
(bin/"beszel-agent-launcher").write <<~EOS
#!/bin/bash
set -a
if [ -f "$HOME/.config/beszel/beszel-agent.env" ]; then
source "$HOME/.config/beszel/beszel-agent.env"
fi
set +a
exec #{bin}/beszel-agent "$@"
EOS
(bin/"beszel-agent-launcher").chmod 0755
service: |
run ["#{bin}/beszel-agent-launcher"]
log_path "#{Dir.home}/.cache/beszel/beszel-agent.log"
error_log_path "#{Dir.home}/.cache/beszel/beszel-agent.log"
keep_alive true
restart_delay 5
process_type :background
winget:
- ids: [beszel-agent]
name: beszel-agent
package_identifier: henrygd.beszel-agent
publisher: henrygd
license: MIT
license_url: "https://github.com/henrygd/beszel/blob/main/LICENSE"
copyright: "2025 henrygd"
homepage: "https://beszel.dev"
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
publisher_support_url: "https://github.com/henrygd/beszel/issues"
short_description: "Agent for Beszel, a lightweight server monitoring platform."
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
description: |
Beszel is a lightweight server monitoring platform that includes Docker
statistics, historical data, and alert functions. It has a friendly web
interface, simple configuration, and is ready to use out of the box.
It supports automatic backup, multi-user, OAuth authentication, and
API access.
tags:
- homelab
- monitoring
- self-hosted
repository:
owner: henrygd
name: beszel-winget
branch: henrygd.beszel-agent-{{ .Version }}
token: "{{ .Env.WINGET_TOKEN }}"
# pull_request:
# enabled: true
# draft: false
# base:
# owner: microsoft
# name: winget-pkgs
# branch: master
release:
draft: true
changelog:
disable: true
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 henrygd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Default OS/ARCH values
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
# Skip building the web UI if true
SKIP_WEB ?= false
# Controls NVML/glibc agent build tag behavior:
# - auto (default): enable on linux/amd64 glibc hosts
# - true: always enable
# - false: always disable
NVML ?= auto
# Detect glibc host for local linux/amd64 builds.
HOST_GLIBC := $(shell \
if [ "$(OS)" = "linux" ] && [ "$(ARCH)" = "amd64" ]; then \
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" ] && { echo true; exit 0; }; \
done; \
if command -v ldd >/dev/null 2>&1; then \
if ldd --version 2>&1 | tr '[:upper:]' '[:lower:]' | awk '/gnu libc|glibc/{found=1} END{exit !found}'; then \
echo true; \
else \
echo false; \
fi; \
else \
echo false; \
fi; \
else \
echo false; \
fi)
# Enable glibc build tag for NVML on supported Linux builds.
AGENT_GO_TAGS :=
ifeq ($(NVML),true)
AGENT_GO_TAGS := -tags glibc
else ifeq ($(NVML),auto)
ifeq ($(HOST_GLIBC),true)
AGENT_GO_TAGS := -tags glibc
endif
endif
# Set executable extension based on target OS
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
.PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales fetch-smartctl-conditional
.DEFAULT_GOAL := build
clean:
go clean
rm -rf ./build
lint:
golangci-lint run
test:
go test -tags=testing ./...
tidy:
go mod tidy
build-web-ui:
@if command -v bun >/dev/null 2>&1; then \
bun install --cwd ./internal/site && \
bun run --cwd ./internal/site build; \
else \
npm install --prefix ./internal/site && \
npm run --prefix ./internal/site build; \
fi
# Conditional .NET build - only for Windows
build-dotnet-conditional:
@if [ "$(OS)" = "windows" ]; then \
echo "Building .NET executable for Windows..."; \
if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./agent/lhm/bin; \
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
else \
echo "Error: dotnet not found. Install .NET SDK to build Windows agent."; \
exit 1; \
fi; \
fi
# Download smartctl.exe at build time for Windows (skips if already present)
fetch-smartctl-conditional:
@if [ "$(OS)" = "windows" ]; then \
go generate -run fetchsmartctl ./agent; \
fi
# Update build-agent to include conditional .NET build
build-agent: tidy build-dotnet-conditional fetch-smartctl-conditional
GOOS=$(OS) GOARCH=$(ARCH) go build $(AGENT_GO_TAGS) -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/agent
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
build-hub-dev: tidy
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./internal/cmd/hub
build: build-agent build-hub
generate-locales:
@if [ ! -f ./internal/site/src/locales/en/en.ts ]; then \
echo "Generating locales..."; \
command -v bun >/dev/null 2>&1 && cd ./internal/site && bun install && bun run sync || cd ./internal/site && npm install && npm run sync; \
fi
dev-server: generate-locales
cd ./internal/site
@if command -v bun >/dev/null 2>&1; then \
cd ./internal/site && bun run dev --host 0.0.0.0; \
else \
cd ./internal/site && npm run dev --host 0.0.0.0; \
fi
dev-hub: export ENV=dev
dev-hub:
mkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html
@if command -v entr >/dev/null 2>&1; then \
find ./internal -type f -name '*.go' | entr -r -s "cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
else \
cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
fi
dev-agent:
@if command -v entr >/dev/null 2>&1; then \
find ./internal/cmd/agent/*.go ./agent/*.go | entr -r go run $(AGENT_GO_TAGS) github.com/henrygd/beszel/internal/cmd/agent; \
else \
go run $(AGENT_GO_TAGS) github.com/henrygd/beszel/internal/cmd/agent; \
fi
build-dotnet:
@if command -v dotnet >/dev/null 2>&1; then \
rm -rf ./agent/lhm/bin; \
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
else \
echo "dotnet not found"; \
fi
# KEY="..." make -j dev
dev: dev-server dev-hub dev-agent
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Reporting a Vulnerability
If you find a vulnerability in the latest version, please [submit a private advisory](https://github.com/henrygd/beszel/security/advisories/new).
If it's low severity (use best judgement) you may open an issue instead of an advisory.
================================================
FILE: agent/agent.go
================================================
// Package agent implements the Beszel monitoring agent that collects and serves system metrics.
//
// The agent runs on monitored systems and communicates collected data
// to the Beszel hub for centralized monitoring and alerting.
package agent
import (
"log/slog"
"strings"
"sync"
"time"
"github.com/gliderlabs/ssh"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/system"
gossh "golang.org/x/crypto/ssh"
)
type Agent struct {
sync.Mutex // Used to lock agent while collecting data
debug bool // true if LOG_LEVEL is set to debug
zfs bool // true if system has arcstats
memCalc string // Memory calculation formula
fsNames []string // List of filesystem device names being monitored
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
diskPrev map[uint16]map[string]prevDisk // Previous disk I/O counters per cache interval
diskUsageCacheDuration time.Duration // How long to cache disk usage (to avoid waking sleeping disks)
lastDiskUsageUpdate time.Time // Last time disk usage was collected
netInterfaces map[string]struct{} // Stores all valid network interfaces
netIoStats map[uint16]system.NetIoStats // Keeps track of bandwidth usage per cache interval
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
dockerManager *dockerManager // Manages Docker API requests
sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info (dynamic)
systemDetails system.Details // Host system details (static, once-per-connection)
gpuManager *GPUManager // Manages GPU data
cache *systemDataCache // Cache for system stats based on cache time
connectionManager *ConnectionManager // Channel to signal connection events
handlerRegistry *HandlerRegistry // Registry for routing incoming messages
server *ssh.Server // SSH server
dataDir string // Directory for persisting data
keys []gossh.PublicKey // SSH public keys
smartManager *SmartManager // Manages SMART data
systemdManager *systemdManager // Manages systemd services
}
// NewAgent creates a new agent with the given data directory for persisting data.
// If the data directory is not set, it will attempt to find the optimal directory.
func NewAgent(dataDir ...string) (agent *Agent, err error) {
agent = &Agent{
fsStats: make(map[string]*system.FsStats),
cache: NewSystemDataCache(),
}
// Initialize disk I/O previous counters storage
agent.diskPrev = make(map[uint16]map[string]prevDisk)
// Initialize per-cache-time network tracking structures
agent.netIoStats = make(map[uint16]system.NetIoStats)
agent.netInterfaceDeltaTrackers = make(map[uint16]*deltatracker.DeltaTracker[string, uint64])
agent.dataDir, err = GetDataDir(dataDir...)
if err != nil {
slog.Warn("Data directory not found")
} else {
slog.Info("Data directory", "path", agent.dataDir)
}
agent.memCalc, _ = utils.GetEnv("MEM_CALC")
agent.sensorConfig = agent.newSensorConfig()
// Parse disk usage cache duration (e.g., "15m", "1h") to avoid waking sleeping disks
if diskUsageCache, exists := utils.GetEnv("DISK_USAGE_CACHE"); exists {
if duration, err := time.ParseDuration(diskUsageCache); err == nil {
agent.diskUsageCacheDuration = duration
slog.Info("DISK_USAGE_CACHE", "duration", duration)
} else {
slog.Warn("Invalid DISK_USAGE_CACHE", "err", err)
}
}
// Set up slog with a log level determined by the LOG_LEVEL env var
if logLevelStr, exists := utils.GetEnv("LOG_LEVEL"); exists {
switch strings.ToLower(logLevelStr) {
case "debug":
agent.debug = true
slog.SetLogLoggerLevel(slog.LevelDebug)
case "warn":
slog.SetLogLoggerLevel(slog.LevelWarn)
case "error":
slog.SetLogLoggerLevel(slog.LevelError)
}
}
slog.Debug(beszel.Version)
// initialize docker manager
agent.dockerManager = newDockerManager()
// initialize system info
agent.refreshSystemDetails()
// SMART_INTERVAL env var to update smart data at this interval
if smartIntervalEnv, exists := utils.GetEnv("SMART_INTERVAL"); exists {
if duration, err := time.ParseDuration(smartIntervalEnv); err == nil && duration > 0 {
agent.systemDetails.SmartInterval = duration
slog.Info("SMART_INTERVAL", "duration", duration)
} else {
slog.Warn("Invalid SMART_INTERVAL", "err", err)
}
}
// initialize connection manager
agent.connectionManager = newConnectionManager(agent)
// initialize handler registry
agent.handlerRegistry = NewHandlerRegistry()
// initialize disk info
agent.initializeDiskInfo()
// initialize net io stats
agent.initializeNetIoStats()
agent.systemdManager, err = newSystemdManager()
if err != nil {
slog.Debug("Systemd", "err", err)
}
agent.smartManager, err = NewSmartManager()
if err != nil {
slog.Debug("SMART", "err", err)
}
// initialize GPU manager
agent.gpuManager, err = NewGPUManager()
if err != nil {
slog.Debug("GPU", "err", err)
}
// if debugging, print stats
if agent.debug {
slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000, IncludeDetails: true}))
}
return agent, nil
}
func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedData {
a.Lock()
defer a.Unlock()
cacheTimeMs := options.CacheTimeMs
data, isCached := a.cache.Get(cacheTimeMs)
if isCached {
slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
return data
}
*data = system.CombinedData{
Stats: a.getSystemStats(cacheTimeMs),
Info: a.systemInfo,
}
// Include static system details only when requested
if options.IncludeDetails {
data.Details = &a.systemDetails
}
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
if a.dockerManager != nil {
if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {
data.Containers = containerStats
slog.Debug("Containers", "data", data.Containers)
} else {
slog.Debug("Containers", "err", err)
}
}
// skip updating systemd services if cache time is not the default 60sec interval
if a.systemdManager != nil && cacheTimeMs == 60_000 {
totalCount := uint16(a.systemdManager.getServiceStatsCount())
if totalCount > 0 {
numFailed := a.systemdManager.getFailedServiceCount()
data.Info.Services = []uint16{totalCount, numFailed}
}
if a.systemdManager.hasFreshStats {
data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
}
}
data.Stats.ExtraFs = make(map[string]*system.FsStats)
data.Info.ExtraFsPct = make(map[string]float64)
for name, stats := range a.fsStats {
if !stats.Root && stats.DiskTotal > 0 {
// Use custom name if available, otherwise use device name
key := name
if stats.Name != "" {
key = stats.Name
}
data.Stats.ExtraFs[key] = stats
// Add percentages to Info struct for dashboard
if stats.DiskTotal > 0 {
pct := utils.TwoDecimals((stats.DiskUsed / stats.DiskTotal) * 100)
data.Info.ExtraFsPct[key] = pct
}
}
}
slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
a.cache.Set(data, cacheTimeMs)
return data
}
// Start initializes and starts the agent with optional WebSocket connection
func (a *Agent) Start(serverOptions ServerOptions) error {
a.keys = serverOptions.Keys
return a.connectionManager.Start(serverOptions)
}
func (a *Agent) getFingerprint() string {
return GetFingerprint(a.dataDir, a.systemDetails.Hostname, a.systemDetails.CpuModel)
}
================================================
FILE: agent/agent_cache.go
================================================
package agent
import (
"sync"
"time"
"github.com/henrygd/beszel/internal/entities/system"
)
type systemDataCache struct {
sync.RWMutex
cache map[uint16]*cacheNode
}
type cacheNode struct {
data *system.CombinedData
lastUpdate time.Time
}
// NewSystemDataCache creates a cache keyed by the polling interval in milliseconds.
func NewSystemDataCache() *systemDataCache {
return &systemDataCache{
cache: make(map[uint16]*cacheNode),
}
}
// Get returns cached combined data when the entry is still considered fresh.
func (c *systemDataCache) Get(cacheTimeMs uint16) (stats *system.CombinedData, isCached bool) {
c.RLock()
defer c.RUnlock()
node, ok := c.cache[cacheTimeMs]
if !ok {
return &system.CombinedData{}, false
}
// allowedSkew := time.Second
// isFresh := time.Since(node.lastUpdate) < time.Duration(cacheTimeMs)*time.Millisecond-allowedSkew
// allow a 50% skew of the cache time
isFresh := time.Since(node.lastUpdate) < time.Duration(cacheTimeMs/2)*time.Millisecond
return node.data, isFresh
}
// Set stores the latest combined data snapshot for the given interval.
func (c *systemDataCache) Set(data *system.CombinedData, cacheTimeMs uint16) {
c.Lock()
defer c.Unlock()
node, ok := c.cache[cacheTimeMs]
if !ok {
node = &cacheNode{}
c.cache[cacheTimeMs] = node
}
node.data = data
node.lastUpdate = time.Now()
}
================================================
FILE: agent/agent_cache_test.go
================================================
//go:build testing
package agent
import (
"testing"
"testing/synctest"
"time"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createTestCacheData() *system.CombinedData {
return &system.CombinedData{
Stats: system.Stats{
Cpu: 50.5,
Mem: 8192,
DiskTotal: 100000,
},
Info: system.Info{
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{
{
Name: "test-container",
Cpu: 25.0,
},
},
}
}
func TestNewSystemDataCache(t *testing.T) {
cache := NewSystemDataCache()
require.NotNil(t, cache)
assert.NotNil(t, cache.cache)
assert.Empty(t, cache.cache)
}
func TestCacheGetSet(t *testing.T) {
cache := NewSystemDataCache()
data := createTestCacheData()
// Test setting data
cache.Set(data, 1000) // 1 second cache
// Test getting fresh data
retrieved, isCached := cache.Get(1000)
assert.True(t, isCached)
assert.Equal(t, data, retrieved)
// Test getting non-existent cache key
_, isCached = cache.Get(2000)
assert.False(t, isCached)
}
func TestCacheFreshness(t *testing.T) {
cache := NewSystemDataCache()
data := createTestCacheData()
testCases := []struct {
name string
cacheTimeMs uint16
sleepMs time.Duration
expectFresh bool
}{
{
name: "fresh data - well within cache time",
cacheTimeMs: 1000, // 1 second
sleepMs: 100, // 100ms
expectFresh: true,
},
{
name: "fresh data - at 50% of cache time boundary",
cacheTimeMs: 1000, // 1 second, 50% = 500ms
sleepMs: 499, // just under 500ms
expectFresh: true,
},
{
name: "stale data - exactly at 50% cache time",
cacheTimeMs: 1000, // 1 second, 50% = 500ms
sleepMs: 500, // exactly 500ms
expectFresh: false,
},
{
name: "stale data - well beyond cache time",
cacheTimeMs: 1000, // 1 second
sleepMs: 800, // 800ms
expectFresh: false,
},
{
name: "short cache time",
cacheTimeMs: 200, // 200ms, 50% = 100ms
sleepMs: 150, // 150ms > 100ms
expectFresh: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
// Set data
cache.Set(data, tc.cacheTimeMs)
// Wait for the specified duration
if tc.sleepMs > 0 {
time.Sleep(tc.sleepMs * time.Millisecond)
}
// Check freshness
_, isCached := cache.Get(tc.cacheTimeMs)
assert.Equal(t, tc.expectFresh, isCached)
})
})
}
}
func TestCacheMultipleIntervals(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
cache := NewSystemDataCache()
data1 := createTestCacheData()
data2 := &system.CombinedData{
Stats: system.Stats{
Cpu: 75.0,
Mem: 16384,
},
Info: system.Info{
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{},
}
// Set data for different intervals
cache.Set(data1, 500) // 500ms cache
cache.Set(data2, 1000) // 1000ms cache
// Both should be fresh immediately
retrieved1, isCached1 := cache.Get(500)
assert.True(t, isCached1)
assert.Equal(t, data1, retrieved1)
retrieved2, isCached2 := cache.Get(1000)
assert.True(t, isCached2)
assert.Equal(t, data2, retrieved2)
// Wait 300ms - 500ms cache should be stale (250ms threshold), 1000ms should still be fresh (500ms threshold)
time.Sleep(300 * time.Millisecond)
_, isCached1 = cache.Get(500)
assert.False(t, isCached1)
_, isCached2 = cache.Get(1000)
assert.True(t, isCached2)
// Wait another 300ms (total 600ms) - now 1000ms cache should also be stale
time.Sleep(300 * time.Millisecond)
_, isCached2 = cache.Get(1000)
assert.False(t, isCached2)
})
}
func TestCacheOverwrite(t *testing.T) {
cache := NewSystemDataCache()
data1 := createTestCacheData()
data2 := &system.CombinedData{
Stats: system.Stats{
Cpu: 90.0,
Mem: 32768,
},
Info: system.Info{
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{},
}
// Set initial data
cache.Set(data1, 1000)
retrieved, isCached := cache.Get(1000)
assert.True(t, isCached)
assert.Equal(t, data1, retrieved)
// Overwrite with new data
cache.Set(data2, 1000)
retrieved, isCached = cache.Get(1000)
assert.True(t, isCached)
assert.Equal(t, data2, retrieved)
assert.NotEqual(t, data1, retrieved)
}
func TestCacheMiss(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
cache := NewSystemDataCache()
// Test getting from empty cache
_, isCached := cache.Get(1000)
assert.False(t, isCached)
// Set data for one interval
data := createTestCacheData()
cache.Set(data, 1000)
// Test getting different interval
_, isCached = cache.Get(2000)
assert.False(t, isCached)
// Test getting after data has expired
time.Sleep(600 * time.Millisecond) // 600ms > 500ms (50% of 1000ms)
_, isCached = cache.Get(1000)
assert.False(t, isCached)
})
}
func TestCacheZeroInterval(t *testing.T) {
cache := NewSystemDataCache()
data := createTestCacheData()
// Set with zero interval - should allow immediate cache
cache.Set(data, 0)
// With 0 interval, 50% is 0, so it should never be considered fresh
// (time.Since(lastUpdate) >= 0, which is not < 0)
_, isCached := cache.Get(0)
assert.False(t, isCached)
}
func TestCacheLargeInterval(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
cache := NewSystemDataCache()
data := createTestCacheData()
// Test with maximum uint16 value
cache.Set(data, 65535) // ~65 seconds
// Should be fresh immediately
_, isCached := cache.Get(65535)
assert.True(t, isCached)
// Should still be fresh after a short time
time.Sleep(100 * time.Millisecond)
_, isCached = cache.Get(65535)
assert.True(t, isCached)
})
}
================================================
FILE: agent/agent_test_helpers.go
================================================
//go:build testing
package agent
// TESTING ONLY: GetConnectionManager is a helper function to get the connection manager for testing.
func (a *Agent) GetConnectionManager() *ConnectionManager {
return a.connectionManager
}
================================================
FILE: agent/battery/battery.go
================================================
//go:build !freebsd
// Package battery provides functions to check if the system has a battery and to get the battery stats.
package battery
import (
"errors"
"log/slog"
"math"
"github.com/distatus/battery"
)
var (
systemHasBattery = false
haveCheckedBattery = false
)
// HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool {
if haveCheckedBattery {
return systemHasBattery
}
haveCheckedBattery = true
batteries, err := battery.GetAll()
for _, bat := range batteries {
if bat != nil && (bat.Full > 0 || bat.Design > 0) {
systemHasBattery = true
break
}
}
if !systemHasBattery {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery
}
// GetBatteryStats returns the current battery percent and charge state
// percent = (current charge of all batteries) / (sum of designed/full capacity of all batteries)
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !HasReadableBattery() {
return batteryPercent, batteryState, errors.ErrUnsupported
}
batteries, err := battery.GetAll()
// we'll handle errors later by skipping batteries with errors, rather
// than skipping everything because of the presence of some errors.
if len(batteries) == 0 {
return batteryPercent, batteryState, errors.New("no batteries")
}
totalCapacity := float64(0)
totalCharge := float64(0)
errs, partialErrs := err.(battery.Errors)
batteryState = math.MaxUint8
for i, bat := range batteries {
if partialErrs && errs[i] != nil {
// if there were some errors, like missing data, skip it
continue
}
if bat == nil || bat.Full == 0 {
// skip batteries with no capacity. Charge is unlikely to ever be zero, but
// we can't guarantee that, so don't skip based on charge.
continue
}
totalCapacity += bat.Full
totalCharge += min(bat.Current, bat.Full)
if bat.State.Raw >= 0 {
batteryState = uint8(bat.State.Raw)
}
}
if totalCapacity == 0 || batteryState == math.MaxUint8 {
// for macs there's sometimes a ghost battery with 0 capacity
// https://github.com/distatus/battery/issues/34
// Instead of skipping over those batteries, we'll check for total 0 capacity
// and return an error. This also prevents a divide by zero.
return batteryPercent, batteryState, errors.New("no battery capacity")
}
batteryPercent = uint8(totalCharge / totalCapacity * 100)
return batteryPercent, batteryState, nil
}
================================================
FILE: agent/battery/battery_freebsd.go
================================================
//go:build freebsd
package battery
import "errors"
func HasReadableBattery() bool {
return false
}
func GetBatteryStats() (uint8, uint8, error) {
return 0, 0, errors.ErrUnsupported
}
================================================
FILE: agent/client.go
================================================
package agent
import (
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/common"
"github.com/fxamacker/cbor/v2"
"github.com/lxzan/gws"
"golang.org/x/crypto/ssh"
)
const (
wsDeadline = 70 * time.Second
)
// WebSocketClient manages the WebSocket connection between the agent and hub.
// It handles authentication, message routing, and connection lifecycle management.
type WebSocketClient struct {
gws.BuiltinEventHandler
options *gws.ClientOption // WebSocket client configuration options
agent *Agent // Reference to the parent agent
Conn *gws.Conn // Active WebSocket connection
hubURL *url.URL // Parsed hub URL for connection
token string // Authentication token for hub registration
fingerprint string // System fingerprint for identification
hubRequest *common.HubRequest[cbor.RawMessage] // Reusable request structure for message parsing
lastConnectAttempt time.Time // Timestamp of last connection attempt
hubVerified bool // Whether the hub has been cryptographically verified
}
// newWebSocketClient creates a new WebSocket client for the given agent.
// It reads configuration from environment variables and validates the hub URL.
func newWebSocketClient(agent *Agent) (client *WebSocketClient, err error) {
hubURLStr, exists := utils.GetEnv("HUB_URL")
if !exists {
return nil, errors.New("HUB_URL environment variable not set")
}
client = &WebSocketClient{}
client.hubURL, err = url.Parse(hubURLStr)
if err != nil {
return nil, errors.New("invalid hub URL")
}
// get registration token
client.token, err = getToken()
if err != nil {
return nil, err
}
client.agent = agent
client.hubRequest = &common.HubRequest[cbor.RawMessage]{}
client.fingerprint = agent.getFingerprint()
return client, nil
}
// getToken returns the token for the WebSocket client.
// It first checks the TOKEN environment variable, then the TOKEN_FILE environment variable.
// If neither is set, it returns an error.
func getToken() (string, error) {
// get token from env var
token, _ := utils.GetEnv("TOKEN")
if token != "" {
return token, nil
}
// get token from file
tokenFile, _ := utils.GetEnv("TOKEN_FILE")
if tokenFile == "" {
return "", errors.New("must set TOKEN or TOKEN_FILE")
}
tokenBytes, err := os.ReadFile(tokenFile)
if err != nil {
return "", err
}
return strings.TrimSpace(string(tokenBytes)), nil
}
// getOptions returns the WebSocket client options, creating them if necessary.
// It configures the connection URL, TLS settings, and authentication headers.
func (client *WebSocketClient) getOptions() *gws.ClientOption {
if client.options != nil {
return client.options
}
// update the hub url to use websocket scheme and api path
if client.hubURL.Scheme == "https" {
client.hubURL.Scheme = "wss"
} else {
client.hubURL.Scheme = "ws"
}
client.hubURL.Path = path.Join(client.hubURL.Path, "api/beszel/agent-connect")
client.options = &gws.ClientOption{
Addr: client.hubURL.String(),
TlsConfig: &tls.Config{InsecureSkipVerify: true},
RequestHeader: http.Header{
"User-Agent": []string{getUserAgent()},
"X-Token": []string{client.token},
"X-Beszel": []string{beszel.Version},
},
}
return client.options
}
// Connect establishes a WebSocket connection to the hub.
// It closes any existing connection before attempting to reconnect.
func (client *WebSocketClient) Connect() (err error) {
client.lastConnectAttempt = time.Now()
// make sure previous connection is closed
client.Close()
client.Conn, _, err = gws.NewClient(client, client.getOptions())
if err != nil {
return err
}
go client.Conn.ReadLoop()
return nil
}
// OnOpen handles WebSocket connection establishment.
// It sets a deadline for the connection to prevent hanging.
func (client *WebSocketClient) OnOpen(conn *gws.Conn) {
conn.SetDeadline(time.Now().Add(wsDeadline))
}
// OnClose handles WebSocket connection closure.
// It logs the closure reason and notifies the connection manager.
func (client *WebSocketClient) OnClose(conn *gws.Conn, err error) {
if err != nil {
slog.Warn("Connection closed", "err", strings.TrimPrefix(err.Error(), "gws: "))
}
client.agent.connectionManager.eventChan <- WebSocketDisconnect
}
// OnMessage handles incoming WebSocket messages from the hub.
// It decodes CBOR messages and routes them to appropriate handlers.
func (client *WebSocketClient) OnMessage(conn *gws.Conn, message *gws.Message) {
defer message.Close()
conn.SetDeadline(time.Now().Add(wsDeadline))
if message.Opcode != gws.OpcodeBinary {
return
}
var HubRequest common.HubRequest[cbor.RawMessage]
err := cbor.Unmarshal(message.Data.Bytes(), &HubRequest)
if err != nil {
slog.Error("Error parsing message", "err", err)
return
}
if err := client.handleHubRequest(&HubRequest, HubRequest.Id); err != nil {
slog.Error("Error handling message", "err", err)
}
}
// OnPing handles WebSocket ping frames.
// It responds with a pong and updates the connection deadline.
func (client *WebSocketClient) OnPing(conn *gws.Conn, message []byte) {
conn.SetDeadline(time.Now().Add(wsDeadline))
conn.WritePong(message)
}
// handleAuthChallenge verifies the authenticity of the hub and returns the system's fingerprint.
func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.RawMessage], requestID *uint32) (err error) {
var authRequest common.FingerprintRequest
if err := cbor.Unmarshal(msg.Data, &authRequest); err != nil {
return err
}
if err := client.verifySignature(authRequest.Signature); err != nil {
return err
}
client.hubVerified = true
client.agent.connectionManager.eventChan <- WebSocketConnect
response := &common.FingerprintResponse{
Fingerprint: client.fingerprint,
}
if authRequest.NeedSysInfo {
response.Name, _ = utils.GetEnv("SYSTEM_NAME")
response.Hostname = client.agent.systemDetails.Hostname
serverAddr := client.agent.connectionManager.serverOptions.Addr
_, response.Port, _ = net.SplitHostPort(serverAddr)
}
return client.sendResponse(response, requestID)
}
// verifySignature verifies the signature of the token using the public keys.
func (client *WebSocketClient) verifySignature(signature []byte) (err error) {
for _, pubKey := range client.agent.keys {
sig := ssh.Signature{
Format: pubKey.Type(),
Blob: signature,
}
if err = pubKey.Verify([]byte(client.token), &sig); err == nil {
return nil
}
}
return errors.New("invalid signature - check KEY value")
}
// Close closes the WebSocket connection gracefully.
// This method is safe to call multiple times.
func (client *WebSocketClient) Close() {
if client.Conn != nil {
_ = client.Conn.WriteClose(1000, nil)
}
}
// handleHubRequest routes the request to the appropriate handler using the handler registry.
func (client *WebSocketClient) handleHubRequest(msg *common.HubRequest[cbor.RawMessage], requestID *uint32) error {
ctx := &HandlerContext{
Client: client,
Agent: client.agent,
Request: msg,
RequestID: requestID,
HubVerified: client.hubVerified,
SendResponse: client.sendResponse,
}
return client.agent.handlerRegistry.Handle(ctx)
}
// sendMessage encodes the given data to CBOR and sends it as a binary message over the WebSocket connection to the hub.
func (client *WebSocketClient) sendMessage(data any) error {
bytes, err := cbor.Marshal(data)
if err != nil {
return err
}
err = client.Conn.WriteMessage(gws.OpcodeBinary, bytes)
if err != nil {
// If writing fails (e.g., broken pipe due to network issues),
// close the connection to trigger reconnection logic (#1263)
client.Close()
}
return err
}
// sendResponse sends a response with optional request ID.
// For ID-based requests, we must populate legacy typed fields for backward
// compatibility with older hubs (<= 0.17) that don't read the generic Data field.
func (client *WebSocketClient) sendResponse(data any, requestID *uint32) error {
if requestID != nil {
response := newAgentResponse(data, requestID)
return client.sendMessage(response)
}
// Legacy format - send data directly
return client.sendMessage(data)
}
// getUserAgent returns one of two User-Agent strings based on current time.
// This is used to avoid being blocked by Cloudflare or other anti-bot measures.
func getUserAgent() string {
const (
uaBase = "Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
uaWindows = "Windows NT 11.0; Win64; x64"
uaMac = "Macintosh; Intel Mac OS X 14_0_0"
)
if time.Now().UnixNano()%2 == 0 {
return fmt.Sprintf(uaBase, uaWindows)
}
return fmt.Sprintf(uaBase, uaMac)
}
================================================
FILE: agent/client_test.go
================================================
//go:build testing
package agent
import (
"crypto/ed25519"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/internal/common"
"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
// TestNewWebSocketClient tests WebSocket client creation
func TestNewWebSocketClient(t *testing.T) {
agent := createTestAgent(t)
testCases := []struct {
name string
hubURL string
token string
expectError bool
errorMsg string
}{
{
name: "valid configuration",
hubURL: "http://localhost:8080",
token: "test-token-123",
expectError: false,
},
{
name: "valid https URL",
hubURL: "https://hub.example.com",
token: "secure-token",
expectError: false,
},
{
name: "missing hub URL",
hubURL: "",
token: "test-token",
expectError: true,
errorMsg: "HUB_URL environment variable not set",
},
{
name: "invalid URL",
hubURL: "ht\ttp://invalid",
token: "test-token",
expectError: true,
errorMsg: "invalid hub URL",
},
{
name: "missing token",
hubURL: "http://localhost:8080",
token: "",
expectError: true,
errorMsg: "must set TOKEN or TOKEN_FILE",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up environment
if tc.hubURL != "" {
os.Setenv("BESZEL_AGENT_HUB_URL", tc.hubURL)
} else {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
}
if tc.token != "" {
os.Setenv("BESZEL_AGENT_TOKEN", tc.token)
} else {
os.Unsetenv("BESZEL_AGENT_TOKEN")
}
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent)
if tc.expectError {
assert.Error(t, err)
if err != nil && tc.errorMsg != "" {
assert.Contains(t, err.Error(), tc.errorMsg)
}
assert.Nil(t, client)
} else {
require.NoError(t, err)
assert.NotNil(t, client)
assert.Equal(t, agent, client.agent)
assert.Equal(t, tc.token, client.token)
assert.Equal(t, tc.hubURL, client.hubURL.String())
assert.NotEmpty(t, client.fingerprint)
assert.NotNil(t, client.hubRequest)
}
})
}
}
// TestWebSocketClient_GetOptions tests WebSocket client options configuration
func TestWebSocketClient_GetOptions(t *testing.T) {
agent := createTestAgent(t)
testCases := []struct {
name string
inputURL string
expectedScheme string
expectedPath string
}{
{
name: "http to ws conversion",
inputURL: "http://localhost:8080",
expectedScheme: "ws",
expectedPath: "/api/beszel/agent-connect",
},
{
name: "https to wss conversion",
inputURL: "https://hub.example.com",
expectedScheme: "wss",
expectedPath: "/api/beszel/agent-connect",
},
{
name: "existing path preservation",
inputURL: "http://localhost:8080/custom/path",
expectedScheme: "ws",
expectedPath: "/custom/path/api/beszel/agent-connect",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", tc.inputURL)
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent)
require.NoError(t, err)
options := client.getOptions()
// Parse the WebSocket URL
wsURL, err := url.Parse(options.Addr)
require.NoError(t, err)
assert.Equal(t, tc.expectedScheme, wsURL.Scheme)
assert.Equal(t, tc.expectedPath, wsURL.Path)
// Check headers
assert.Equal(t, "test-token", options.RequestHeader.Get("X-Token"))
assert.Equal(t, beszel.Version, options.RequestHeader.Get("X-Beszel"))
assert.Contains(t, options.RequestHeader.Get("User-Agent"), "Mozilla/5.0")
// Test options caching
options2 := client.getOptions()
assert.Same(t, options, options2, "Options should be cached")
})
}
}
// TestWebSocketClient_VerifySignature tests signature verification
func TestWebSocketClient_VerifySignature(t *testing.T) {
agent := createTestAgent(t)
// Generate test key pairs
_, goodPrivKey, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
goodPubKey, err := ssh.NewPublicKey(goodPrivKey.Public().(ed25519.PublicKey))
require.NoError(t, err)
_, badPrivKey, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
badPubKey, err := ssh.NewPublicKey(badPrivKey.Public().(ed25519.PublicKey))
require.NoError(t, err)
// Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent)
require.NoError(t, err)
testCases := []struct {
name string
keys []ssh.PublicKey
token string
signWith ed25519.PrivateKey
expectError bool
}{
{
name: "valid signature with correct key",
keys: []ssh.PublicKey{goodPubKey},
token: "test-token",
signWith: goodPrivKey,
expectError: false,
},
{
name: "invalid signature with wrong key",
keys: []ssh.PublicKey{goodPubKey},
token: "test-token",
signWith: badPrivKey,
expectError: true,
},
{
name: "valid signature with multiple keys",
keys: []ssh.PublicKey{badPubKey, goodPubKey},
token: "test-token",
signWith: goodPrivKey,
expectError: false,
},
{
name: "no valid keys",
keys: []ssh.PublicKey{badPubKey},
token: "test-token",
signWith: goodPrivKey,
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up agent with test keys
agent.keys = tc.keys
client.token = tc.token
// Create signature
signature := ed25519.Sign(tc.signWith, []byte(tc.token))
err := client.verifySignature(signature)
if tc.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid signature")
} else {
assert.NoError(t, err)
}
})
}
}
// TestWebSocketClient_HandleHubRequest tests hub request routing (basic verification logic)
func TestWebSocketClient_HandleHubRequest(t *testing.T) {
agent := createTestAgent(t)
// Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent)
require.NoError(t, err)
testCases := []struct {
name string
action common.WebSocketAction
hubVerified bool
expectError bool
errorMsg string
}{
{
name: "CheckFingerprint without verification",
action: common.CheckFingerprint,
hubVerified: false,
expectError: false, // CheckFingerprint is allowed without verification
},
{
name: "GetData without verification",
action: common.GetData,
hubVerified: false,
expectError: true,
errorMsg: "hub not verified",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client.hubVerified = tc.hubVerified
// Create minimal request
hubRequest := &common.HubRequest[cbor.RawMessage]{
Action: tc.action,
Data: cbor.RawMessage{},
}
err := client.handleHubRequest(hubRequest, nil)
if tc.expectError {
assert.Error(t, err)
if tc.errorMsg != "" {
assert.Contains(t, err.Error(), tc.errorMsg)
}
} else {
// For CheckFingerprint, we expect a decode error since we're not providing valid data,
// but it shouldn't be the "hub not verified" error
if err != nil && tc.errorMsg != "" {
assert.NotContains(t, err.Error(), tc.errorMsg)
}
}
})
}
}
// TestWebSocketClient_GetUserAgent tests user agent generation
func TestGetUserAgent(t *testing.T) {
// Run multiple times to check both variants
userAgents := make(map[string]bool)
for range 20 {
ua := getUserAgent()
userAgents[ua] = true
// Check that it's a valid Mozilla user agent
assert.Contains(t, ua, "Mozilla/5.0")
assert.Contains(t, ua, "AppleWebKit/537.36")
assert.Contains(t, ua, "Chrome/124.0.0.0")
assert.Contains(t, ua, "Safari/537.36")
// Should contain either Windows or Mac
isWindows := strings.Contains(ua, "Windows NT 11.0")
isMac := strings.Contains(ua, "Macintosh; Intel Mac OS X 14_0_0")
assert.True(t, isWindows || isMac, "User agent should contain either Windows or Mac identifier")
}
// With enough iterations, we should see both variants
// though this might occasionally fail
if len(userAgents) == 1 {
t.Log("Note: Only one user agent variant was generated in this test run")
}
}
// TestWebSocketClient_Close tests connection closing
func TestWebSocketClient_Close(t *testing.T) {
agent := createTestAgent(t)
// Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent)
require.NoError(t, err)
// Test closing with nil connection (should not panic)
assert.NotPanics(t, func() {
client.Close()
})
}
// TestWebSocketClient_ConnectRateLimit tests connection rate limiting
func TestWebSocketClient_ConnectRateLimit(t *testing.T) {
agent := createTestAgent(t)
// Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent)
require.NoError(t, err)
// Set recent connection attempt
client.lastConnectAttempt = time.Now()
// Test that connection fails quickly due to rate limiting
// This won't actually connect but should fail fast
err = client.Connect()
assert.Error(t, err, "Connection should fail but not hang")
}
// TestGetToken tests the getToken function with various scenarios
func TestGetToken(t *testing.T) {
unsetEnvVars := func() {
os.Unsetenv("BESZEL_AGENT_TOKEN")
os.Unsetenv("TOKEN")
os.Unsetenv("BESZEL_AGENT_TOKEN_FILE")
os.Unsetenv("TOKEN_FILE")
}
t.Run("token from TOKEN environment variable", func(t *testing.T) {
unsetEnvVars()
// Set TOKEN env var
expectedToken := "test-token-from-env"
os.Setenv("TOKEN", expectedToken)
defer os.Unsetenv("TOKEN")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("token from BESZEL_AGENT_TOKEN environment variable", func(t *testing.T) {
unsetEnvVars()
// Set BESZEL_AGENT_TOKEN env var (should take precedence)
expectedToken := "test-token-from-beszel-env"
os.Setenv("BESZEL_AGENT_TOKEN", expectedToken)
defer os.Unsetenv("BESZEL_AGENT_TOKEN")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("token from TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file
expectedToken := "test-token-from-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(expectedToken)
require.NoError(t, err)
tokenFile.Close()
// Set TOKEN_FILE env var
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("token from BESZEL_AGENT_TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file
expectedToken := "test-token-from-beszel-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(expectedToken)
require.NoError(t, err)
tokenFile.Close()
// Set BESZEL_AGENT_TOKEN_FILE env var (should take precedence)
os.Setenv("BESZEL_AGENT_TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("BESZEL_AGENT_TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token)
})
t.Run("TOKEN takes precedence over TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file
fileToken := "token-from-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(fileToken)
require.NoError(t, err)
tokenFile.Close()
// Set both TOKEN and TOKEN_FILE
envToken := "token-from-env"
os.Setenv("TOKEN", envToken)
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer func() {
os.Unsetenv("TOKEN")
os.Unsetenv("TOKEN_FILE")
}()
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, envToken, token, "TOKEN should take precedence over TOKEN_FILE")
})
t.Run("error when neither TOKEN nor TOKEN_FILE is set", func(t *testing.T) {
unsetEnvVars()
token, err := getToken()
assert.Error(t, err)
assert.Equal(t, "", token)
assert.Contains(t, err.Error(), "must set TOKEN or TOKEN_FILE")
})
t.Run("error when TOKEN_FILE points to non-existent file", func(t *testing.T) {
unsetEnvVars()
// Set TOKEN_FILE to a non-existent file
os.Setenv("TOKEN_FILE", "/non/existent/file.txt")
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.Error(t, err)
assert.Equal(t, "", token)
assert.Contains(t, err.Error(), "no such file or directory")
})
t.Run("handles empty token file", func(t *testing.T) {
unsetEnvVars()
// Create an empty token file
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
tokenFile.Close()
// Set TOKEN_FILE env var
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, "", token, "Empty file should return empty string")
})
t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
tokenWithWhitespace := " test-token-with-whitespace \n\t"
expectedToken := "test-token-with-whitespace"
tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err)
defer os.Remove(tokenFile.Name())
_, err = tokenFile.WriteString(tokenWithWhitespace)
require.NoError(t, err)
tokenFile.Close()
os.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken()
assert.NoError(t, err)
assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content")
})
}
================================================
FILE: agent/connection_manager.go
================================================
package agent
import (
"context"
"errors"
"log/slog"
"os/signal"
"syscall"
"time"
"github.com/henrygd/beszel/agent/health"
"github.com/henrygd/beszel/internal/entities/system"
)
// ConnectionManager manages the connection state and events for the agent.
// It handles both WebSocket and SSH connections, automatically switching between
// them based on availability and managing reconnection attempts.
type ConnectionManager struct {
agent *Agent // Reference to the parent agent
State ConnectionState // Current connection state
eventChan chan ConnectionEvent // Channel for connection events
wsClient *WebSocketClient // WebSocket client for hub communication
serverOptions ServerOptions // Configuration for SSH server
wsTicker *time.Ticker // Ticker for WebSocket connection attempts
isConnecting bool // Prevents multiple simultaneous reconnection attempts
ConnectionType system.ConnectionType
}
// ConnectionState represents the current connection state of the agent.
type ConnectionState uint8
// ConnectionEvent represents connection-related events that can occur.
type ConnectionEvent uint8
// Connection states
const (
Disconnected ConnectionState = iota // No active connection
WebSocketConnected // Connected via WebSocket
SSHConnected // Connected via SSH
)
// Connection events
const (
WebSocketConnect ConnectionEvent = iota // WebSocket connection established
WebSocketDisconnect // WebSocket connection lost
SSHConnect // SSH connection established
SSHDisconnect // SSH connection lost
)
const wsTickerInterval = 10 * time.Second
// newConnectionManager creates a new connection manager for the given agent.
func newConnectionManager(agent *Agent) *ConnectionManager {
cm := &ConnectionManager{
agent: agent,
State: Disconnected,
}
return cm
}
// startWsTicker starts or resets the WebSocket connection attempt ticker.
func (c *ConnectionManager) startWsTicker() {
if c.wsTicker == nil {
c.wsTicker = time.NewTicker(wsTickerInterval)
} else {
c.wsTicker.Reset(wsTickerInterval)
}
}
// stopWsTicker stops the WebSocket connection attempt ticker.
func (c *ConnectionManager) stopWsTicker() {
if c.wsTicker != nil {
c.wsTicker.Stop()
}
}
// Start begins connection attempts and enters the main event loop.
// It handles connection events, periodic health updates, and graceful shutdown.
func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
if c.eventChan != nil {
return errors.New("already started")
}
wsClient, err := newWebSocketClient(c.agent)
if err != nil {
slog.Warn("Error creating WebSocket client", "err", err)
}
c.wsClient = wsClient
c.serverOptions = serverOptions
c.eventChan = make(chan ConnectionEvent, 1)
// signal handling for shutdown
sigCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stopSignals()
c.startWsTicker()
c.connect()
// update health status immediately and every 90 seconds
_ = health.Update()
healthTicker := time.Tick(90 * time.Second)
for {
select {
case connectionEvent := <-c.eventChan:
c.handleEvent(connectionEvent)
case <-c.wsTicker.C:
_ = c.startWebSocketConnection()
case <-healthTicker:
_ = health.Update()
case <-sigCtx.Done():
slog.Info("Shutting down", "cause", context.Cause(sigCtx))
_ = c.agent.StopServer()
c.closeWebSocket()
return health.CleanUp()
}
}
}
// handleEvent processes connection events and updates the connection state accordingly.
func (c *ConnectionManager) handleEvent(event ConnectionEvent) {
switch event {
case WebSocketConnect:
c.handleStateChange(WebSocketConnected)
case SSHConnect:
c.handleStateChange(SSHConnected)
case WebSocketDisconnect:
if c.State == WebSocketConnected {
c.handleStateChange(Disconnected)
}
case SSHDisconnect:
if c.State == SSHConnected {
c.handleStateChange(Disconnected)
}
}
}
// handleStateChange updates the connection state and performs necessary actions
// based on the new state, including stopping services and initiating reconnections.
func (c *ConnectionManager) handleStateChange(newState ConnectionState) {
if c.State == newState {
return
}
c.State = newState
switch newState {
case WebSocketConnected:
slog.Info("WebSocket connected", "host", c.wsClient.hubURL.Host)
c.ConnectionType = system.ConnectionTypeWebSocket
c.stopWsTicker()
_ = c.agent.StopServer()
c.isConnecting = false
case SSHConnected:
// stop new ws connection attempts
slog.Info("SSH connection established")
c.ConnectionType = system.ConnectionTypeSSH
c.stopWsTicker()
c.isConnecting = false
case Disconnected:
c.ConnectionType = system.ConnectionTypeNone
if c.isConnecting {
// Already handling reconnection, avoid duplicate attempts
return
}
c.isConnecting = true
slog.Warn("Disconnected from hub")
// make sure old ws connection is closed
c.closeWebSocket()
// reconnect
go c.connect()
}
}
// connect handles the connection logic with proper delays and priority.
// It attempts WebSocket connection first, falling back to SSH server if needed.
func (c *ConnectionManager) connect() {
c.isConnecting = true
defer func() {
c.isConnecting = false
}()
if c.wsClient != nil && time.Since(c.wsClient.lastConnectAttempt) < 5*time.Second {
time.Sleep(5 * time.Second)
}
// Try WebSocket first, if it fails, start SSH server
err := c.startWebSocketConnection()
if err != nil && c.State == Disconnected {
c.startSSHServer()
c.startWsTicker()
}
}
// startWebSocketConnection attempts to establish a WebSocket connection to the hub.
func (c *ConnectionManager) startWebSocketConnection() error {
if c.State != Disconnected {
return errors.New("already connected")
}
if c.wsClient == nil {
return errors.New("WebSocket client not initialized")
}
if time.Since(c.wsClient.lastConnectAttempt) < 5*time.Second {
return errors.New("already connecting")
}
err := c.wsClient.Connect()
if err != nil {
slog.Warn("WebSocket connection failed", "err", err)
c.closeWebSocket()
}
return err
}
// startSSHServer starts the SSH server if the agent is currently disconnected.
func (c *ConnectionManager) startSSHServer() {
if c.State == Disconnected {
go c.agent.StartServer(c.serverOptions)
}
}
// closeWebSocket closes the WebSocket connection if it exists.
func (c *ConnectionManager) closeWebSocket() {
if c.wsClient != nil {
c.wsClient.Close()
}
}
================================================
FILE: agent/connection_manager_test.go
================================================
//go:build testing
package agent
import (
"crypto/ed25519"
"fmt"
"net"
"net/url"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
func createTestAgent(t *testing.T) *Agent {
dataDir := t.TempDir()
agent, err := NewAgent(dataDir)
require.NoError(t, err)
return agent
}
func createTestServerOptions(t *testing.T) ServerOptions {
// Generate test key pair
_, privKey, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
sshPubKey, err := ssh.NewPublicKey(privKey.Public().(ed25519.PublicKey))
require.NoError(t, err)
// Find available port
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := listener.Addr().(*net.TCPAddr).Port
listener.Close()
return ServerOptions{
Network: "tcp",
Addr: fmt.Sprintf("127.0.0.1:%d", port),
Keys: []ssh.PublicKey{sshPubKey},
}
}
// TestConnectionManager_NewConnectionManager tests connection manager creation
func TestConnectionManager_NewConnectionManager(t *testing.T) {
agent := createTestAgent(t)
cm := newConnectionManager(agent)
assert.NotNil(t, cm, "Connection manager should not be nil")
assert.Equal(t, agent, cm.agent, "Agent reference should be set")
assert.Equal(t, Disconnected, cm.State, "Initial state should be Disconnected")
assert.Nil(t, cm.eventChan, "Event channel should be nil initially")
assert.Nil(t, cm.wsClient, "WebSocket client should be nil initially")
assert.Nil(t, cm.wsTicker, "WebSocket ticker should be nil initially")
assert.False(t, cm.isConnecting, "isConnecting should be false initially")
}
// TestConnectionManager_StateTransitions tests basic state transitions
func TestConnectionManager_StateTransitions(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
initialState := cm.State
cm.wsClient = &WebSocketClient{
hubURL: &url.URL{
Host: "localhost:8080",
},
}
assert.NotNil(t, cm, "Connection manager should not be nil")
assert.Equal(t, Disconnected, initialState, "Initial state should be Disconnected")
// Test state transitions
cm.handleStateChange(WebSocketConnected)
assert.Equal(t, WebSocketConnected, cm.State, "State should change to WebSocketConnected")
cm.handleStateChange(SSHConnected)
assert.Equal(t, SSHConnected, cm.State, "State should change to SSHConnected")
cm.handleStateChange(Disconnected)
assert.Equal(t, Disconnected, cm.State, "State should change to Disconnected")
// Test that same state doesn't trigger changes
cm.State = WebSocketConnected
cm.handleStateChange(WebSocketConnected)
assert.Equal(t, WebSocketConnected, cm.State, "Same state should not trigger change")
}
// TestConnectionManager_EventHandling tests event handling logic
func TestConnectionManager_EventHandling(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
cm.wsClient = &WebSocketClient{
hubURL: &url.URL{
Host: "localhost:8080",
},
}
testCases := []struct {
name string
initialState ConnectionState
event ConnectionEvent
expectedState ConnectionState
}{
{
name: "WebSocket connect from disconnected",
initialState: Disconnected,
event: WebSocketConnect,
expectedState: WebSocketConnected,
},
{
name: "SSH connect from disconnected",
initialState: Disconnected,
event: SSHConnect,
expectedState: SSHConnected,
},
{
name: "WebSocket disconnect from connected",
initialState: WebSocketConnected,
event: WebSocketDisconnect,
expectedState: Disconnected,
},
{
name: "SSH disconnect from connected",
initialState: SSHConnected,
event: SSHDisconnect,
expectedState: Disconnected,
},
{
name: "WebSocket disconnect from SSH connected (no change)",
initialState: SSHConnected,
event: WebSocketDisconnect,
expectedState: SSHConnected,
},
{
name: "SSH disconnect from WebSocket connected (no change)",
initialState: WebSocketConnected,
event: SSHDisconnect,
expectedState: WebSocketConnected,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cm.State = tc.initialState
cm.handleEvent(tc.event)
assert.Equal(t, tc.expectedState, cm.State, "State should match expected after event")
})
}
}
// TestConnectionManager_TickerManagement tests WebSocket ticker management
func TestConnectionManager_TickerManagement(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
// Test starting ticker
cm.startWsTicker()
assert.NotNil(t, cm.wsTicker, "Ticker should be created")
// Test stopping ticker (should not panic)
assert.NotPanics(t, func() {
cm.stopWsTicker()
}, "Stopping ticker should not panic")
// Test stopping nil ticker (should not panic)
cm.wsTicker = nil
assert.NotPanics(t, func() {
cm.stopWsTicker()
}, "Stopping nil ticker should not panic")
// Test restarting ticker
cm.startWsTicker()
assert.NotNil(t, cm.wsTicker, "Ticker should be recreated")
// Test resetting existing ticker
firstTicker := cm.wsTicker
cm.startWsTicker()
assert.Equal(t, firstTicker, cm.wsTicker, "Same ticker instance should be reused")
cm.stopWsTicker()
}
// TestConnectionManager_WebSocketConnectionFlow tests WebSocket connection logic
func TestConnectionManager_WebSocketConnectionFlow(t *testing.T) {
if testing.Short() {
t.Skip("Skipping WebSocket connection test in short mode")
}
agent := createTestAgent(t)
cm := agent.connectionManager
// Test WebSocket connection without proper environment
err := cm.startWebSocketConnection()
assert.Error(t, err, "WebSocket connection should fail without proper environment")
assert.Equal(t, Disconnected, cm.State, "State should remain Disconnected after failed connection")
// Test with invalid URL
os.Setenv("BESZEL_AGENT_HUB_URL", "invalid-url")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Test with missing token
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Unsetenv("BESZEL_AGENT_TOKEN")
_, err2 := newWebSocketClient(agent)
assert.Error(t, err2, "WebSocket client creation should fail without token")
}
// TestConnectionManager_ReconnectionLogic tests reconnection prevention logic
func TestConnectionManager_ReconnectionLogic(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
cm.eventChan = make(chan ConnectionEvent, 1)
// Test that isConnecting flag prevents duplicate reconnection attempts
// Start from connected state, then simulate disconnect
cm.State = WebSocketConnected
cm.isConnecting = false
// First disconnect should trigger reconnection logic
cm.handleStateChange(Disconnected)
assert.Equal(t, Disconnected, cm.State, "Should change to disconnected")
assert.True(t, cm.isConnecting, "Should set isConnecting flag")
}
// TestConnectionManager_ConnectWithRateLimit tests connection rate limiting
func TestConnectionManager_ConnectWithRateLimit(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
// Set up environment for WebSocket client creation
os.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Create WebSocket client
wsClient, err := newWebSocketClient(agent)
require.NoError(t, err)
cm.wsClient = wsClient
// Set recent connection attempt
cm.wsClient.lastConnectAttempt = time.Now()
// Test that connection is rate limited
err = cm.startWebSocketConnection()
assert.Error(t, err, "Should error due to rate limiting")
assert.Contains(t, err.Error(), "already connecting", "Error should indicate rate limiting")
// Test connection after rate limit expires
cm.wsClient.lastConnectAttempt = time.Now().Add(-10 * time.Second)
err = cm.startWebSocketConnection()
// This will fail due to no actual server, but should not be rate limited
assert.Error(t, err, "Connection should fail but not due to rate limiting")
assert.NotContains(t, err.Error(), "already connecting", "Error should not indicate rate limiting")
}
// TestConnectionManager_StartWithInvalidConfig tests starting with invalid configuration
func TestConnectionManager_StartWithInvalidConfig(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
serverOptions := createTestServerOptions(t)
// Test starting when already started
cm.eventChan = make(chan ConnectionEvent, 5)
err := cm.Start(serverOptions)
assert.Error(t, err, "Should error when starting already started connection manager")
}
// TestConnectionManager_CloseWebSocket tests WebSocket closing
func TestConnectionManager_CloseWebSocket(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
// Test closing when no WebSocket client exists
assert.NotPanics(t, func() {
cm.closeWebSocket()
}, "Should not panic when closing nil WebSocket client")
// Set up environment and create WebSocket client
os.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
wsClient, err := newWebSocketClient(agent)
require.NoError(t, err)
cm.wsClient = wsClient
// Test closing when WebSocket client exists
assert.NotPanics(t, func() {
cm.closeWebSocket()
}, "Should not panic when closing WebSocket client")
}
// TestConnectionManager_ConnectFlow tests the connect method
func TestConnectionManager_ConnectFlow(t *testing.T) {
agent := createTestAgent(t)
cm := agent.connectionManager
// Test connect without WebSocket client
assert.NotPanics(t, func() {
cm.connect()
}, "Connect should not panic without WebSocket client")
}
================================================
FILE: agent/cpu.go
================================================
package agent
import (
"math"
"runtime"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/cpu"
)
var lastCpuTimes = make(map[uint16]cpu.TimesStat)
var lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)
// init initializes the CPU monitoring by storing the initial CPU times
// for the default 60-second cache interval.
func init() {
if times, err := cpu.Times(false); err == nil && len(times) > 0 {
lastCpuTimes[60000] = times[0]
}
if perCoreTimes, err := cpu.Times(true); err == nil && len(perCoreTimes) > 0 {
lastPerCoreCpuTimes[60000] = perCoreTimes
}
}
// CpuMetrics contains detailed CPU usage breakdown
type CpuMetrics struct {
Total float64
User float64
System float64
Iowait float64
Steal float64
Idle float64
}
// getCpuMetrics calculates detailed CPU usage metrics using cached previous measurements.
// It returns percentages for total, user, system, iowait, and steal time.
func getCpuMetrics(cacheTimeMs uint16) (CpuMetrics, error) {
times, err := cpu.Times(false)
if err != nil || len(times) == 0 {
return CpuMetrics{}, err
}
// if cacheTimeMs is not in lastCpuTimes, use 60000 as fallback lastCpuTime
if _, ok := lastCpuTimes[cacheTimeMs]; !ok {
lastCpuTimes[cacheTimeMs] = lastCpuTimes[60000]
}
t1 := lastCpuTimes[cacheTimeMs]
t2 := times[0]
t1All, _ := getAllBusy(t1)
t2All, _ := getAllBusy(t2)
totalDelta := t2All - t1All
if totalDelta <= 0 {
return CpuMetrics{}, nil
}
metrics := CpuMetrics{
Total: calculateBusy(t1, t2),
User: clampPercent((t2.User - t1.User) / totalDelta * 100),
System: clampPercent((t2.System - t1.System) / totalDelta * 100),
Iowait: clampPercent((t2.Iowait - t1.Iowait) / totalDelta * 100),
Steal: clampPercent((t2.Steal - t1.Steal) / totalDelta * 100),
Idle: clampPercent((t2.Idle - t1.Idle) / totalDelta * 100),
}
lastCpuTimes[cacheTimeMs] = times[0]
return metrics, nil
}
// clampPercent ensures the percentage is between 0 and 100
func clampPercent(value float64) float64 {
return math.Min(100, math.Max(0, value))
}
// getPerCoreCpuUsage calculates per-core CPU busy usage as integer percentages (0-100).
// It uses cached previous measurements for the provided cache interval.
func getPerCoreCpuUsage(cacheTimeMs uint16) (system.Uint8Slice, error) {
perCoreTimes, err := cpu.Times(true)
if err != nil || len(perCoreTimes) == 0 {
return nil, err
}
// Initialize cache if needed
if _, ok := lastPerCoreCpuTimes[cacheTimeMs]; !ok {
lastPerCoreCpuTimes[cacheTimeMs] = lastPerCoreCpuTimes[60000]
}
lastTimes := lastPerCoreCpuTimes[cacheTimeMs]
// Limit to the number of cores available in both samples
length := min(len(lastTimes), len(perCoreTimes))
usage := make([]uint8, length)
for i := 0; i < length; i++ {
t1 := lastTimes[i]
t2 := perCoreTimes[i]
usage[i] = uint8(math.Round(calculateBusy(t1, t2)))
}
lastPerCoreCpuTimes[cacheTimeMs] = perCoreTimes
return usage, nil
}
// calculateBusy calculates the CPU busy percentage between two time points.
// It computes the ratio of busy time to total time elapsed between t1 and t2,
// returning a percentage clamped between 0 and 100.
func calculateBusy(t1, t2 cpu.TimesStat) float64 {
t1All, t1Busy := getAllBusy(t1)
t2All, t2Busy := getAllBusy(t2)
if t2All <= t1All || t2Busy <= t1Busy {
return 0
}
return clampPercent((t2Busy - t1Busy) / (t2All - t1All) * 100)
}
// getAllBusy calculates the total CPU time and busy CPU time from CPU times statistics.
// On Linux, it excludes guest and guest_nice time from the total to match kernel behavior.
// Returns total CPU time and busy CPU time (total minus idle and I/O wait time).
func getAllBusy(t cpu.TimesStat) (float64, float64) {
tot := t.Total()
if runtime.GOOS == "linux" {
tot -= t.Guest // Linux 2.6.24+
tot -= t.GuestNice // Linux 3.2.0+
}
busy := tot - t.Idle - t.Iowait
return tot, busy
}
================================================
FILE: agent/data_dir.go
================================================
package agent
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/henrygd/beszel/agent/utils"
)
// GetDataDir returns the path to the data directory for the agent and an error
// if the directory is not valid. Attempts to find the optimal data directory if
// no data directories are provided.
func GetDataDir(dataDirs ...string) (string, error) {
if len(dataDirs) > 0 {
return testDataDirs(dataDirs)
}
dataDir, _ := utils.GetEnv("DATA_DIR")
if dataDir != "" {
dataDirs = append(dataDirs, dataDir)
}
if runtime.GOOS == "windows" {
dataDirs = append(dataDirs,
filepath.Join(os.Getenv("APPDATA"), "beszel-agent"),
filepath.Join(os.Getenv("LOCALAPPDATA"), "beszel-agent"),
)
} else {
dataDirs = append(dataDirs, "/var/lib/beszel-agent")
if homeDir, err := os.UserHomeDir(); err == nil {
dataDirs = append(dataDirs, filepath.Join(homeDir, ".config", "beszel"))
}
}
return testDataDirs(dataDirs)
}
func testDataDirs(paths []string) (string, error) {
// first check if the directory exists and is writable
for _, path := range paths {
if valid, _ := isValidDataDir(path, false); valid {
return path, nil
}
}
// if the directory doesn't exist, try to create it
for _, path := range paths {
exists, _ := directoryExists(path)
if exists {
continue
}
if err := os.MkdirAll(path, 0755); err != nil {
continue
}
// Verify the created directory is actually writable
writable, _ := directoryIsWritable(path)
if !writable {
continue
}
return path, nil
}
return "", errors.New("data directory not found")
}
func isValidDataDir(path string, createIfNotExists bool) (bool, error) {
exists, err := directoryExists(path)
if err != nil {
return false, err
}
if !exists {
if !createIfNotExists {
return false, nil
}
if err = os.MkdirAll(path, 0755); err != nil {
return false, err
}
}
// Always check if the directory is writable
writable, err := directoryIsWritable(path)
if err != nil {
return false, err
}
return writable, nil
}
// directoryExists checks if a directory exists
func directoryExists(path string) (bool, error) {
// Check if directory exists
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if !stat.IsDir() {
return false, fmt.Errorf("%s is not a directory", path)
}
return true, nil
}
// directoryIsWritable tests if a directory is writable by creating and removing a temporary file
func directoryIsWritable(path string) (bool, error) {
testFile := filepath.Join(path, ".write-test")
file, err := os.Create(testFile)
if err != nil {
return false, err
}
defer file.Close()
defer os.Remove(testFile)
return true, nil
}
================================================
FILE: agent/data_dir_test.go
================================================
//go:build testing
package agent
import (
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetDataDir(t *testing.T) {
// Test with explicit dataDir parameter
t.Run("explicit data dir", func(t *testing.T) {
tempDir := t.TempDir()
result, err := GetDataDir(tempDir)
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
// Test with explicit non-existent dataDir that can be created
t.Run("explicit data dir - create new", func(t *testing.T) {
tempDir := t.TempDir()
newDir := filepath.Join(tempDir, "new-data-dir")
result, err := GetDataDir(newDir)
require.NoError(t, err)
assert.Equal(t, newDir, result)
// Verify directory was created
stat, err := os.Stat(newDir)
require.NoError(t, err)
assert.True(t, stat.IsDir())
})
// Test with DATA_DIR environment variable
t.Run("DATA_DIR environment variable", func(t *testing.T) {
tempDir := t.TempDir()
// Set environment variable
oldValue := os.Getenv("DATA_DIR")
defer func() {
if oldValue == "" {
os.Unsetenv("BESZEL_AGENT_DATA_DIR")
} else {
os.Setenv("BESZEL_AGENT_DATA_DIR", oldValue)
}
}()
os.Setenv("BESZEL_AGENT_DATA_DIR", tempDir)
result, err := GetDataDir()
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
// Test with invalid explicit dataDir
t.Run("invalid explicit data dir", func(t *testing.T) {
invalidPath := "/invalid/path/that/cannot/be/created"
_, err := GetDataDir(invalidPath)
assert.Error(t, err)
})
// Test fallback behavior (empty dataDir, no env var)
t.Run("fallback to default directories", func(t *testing.T) {
// Clear DATA_DIR environment variable
oldValue := os.Getenv("DATA_DIR")
defer func() {
if oldValue == "" {
os.Unsetenv("DATA_DIR")
} else {
os.Setenv("DATA_DIR", oldValue)
}
}()
os.Unsetenv("DATA_DIR")
// This will try platform-specific defaults, which may or may not work
// We're mainly testing that it doesn't panic and returns some result
result, err := GetDataDir()
// We don't assert success/failure here since it depends on system permissions
// Just verify we get a string result if no error
if err == nil {
assert.NotEmpty(t, result)
}
})
}
func TestTestDataDirs(t *testing.T) {
// Test with existing valid directory
t.Run("existing valid directory", func(t *testing.T) {
tempDir := t.TempDir()
result, err := testDataDirs([]string{tempDir})
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
// Test with multiple directories, first one valid
t.Run("multiple dirs - first valid", func(t *testing.T) {
tempDir := t.TempDir()
invalidDir := "/invalid/path"
result, err := testDataDirs([]string{tempDir, invalidDir})
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
// Test with multiple directories, second one valid
t.Run("multiple dirs - second valid", func(t *testing.T) {
tempDir := t.TempDir()
invalidDir := "/invalid/path"
result, err := testDataDirs([]string{invalidDir, tempDir})
require.NoError(t, err)
assert.Equal(t, tempDir, result)
})
// Test with non-existing directory that can be created
t.Run("create new directory", func(t *testing.T) {
tempDir := t.TempDir()
newDir := filepath.Join(tempDir, "new-dir")
result, err := testDataDirs([]string{newDir})
require.NoError(t, err)
assert.Equal(t, newDir, result)
// Verify directory was created
stat, err := os.Stat(newDir)
require.NoError(t, err)
assert.True(t, stat.IsDir())
})
// Test with no valid directories
t.Run("no valid directories", func(t *testing.T) {
invalidPaths := []string{"/invalid/path1", "/invalid/path2"}
_, err := testDataDirs(invalidPaths)
assert.Error(t, err)
assert.Contains(t, err.Error(), "data directory not found")
})
}
func TestIsValidDataDir(t *testing.T) {
// Test with existing directory
t.Run("existing directory", func(t *testing.T) {
tempDir := t.TempDir()
valid, err := isValidDataDir(tempDir, false)
require.NoError(t, err)
assert.True(t, valid)
})
// Test with non-existing directory, createIfNotExists=false
t.Run("non-existing dir - no create", func(t *testing.T) {
tempDir := t.TempDir()
nonExistentDir := filepath.Join(tempDir, "does-not-exist")
valid, err := isValidDataDir(nonExistentDir, false)
require.NoError(t, err)
assert.False(t, valid)
})
// Test with non-existing directory, createIfNotExists=true
t.Run("non-existing dir - create", func(t *testing.T) {
tempDir := t.TempDir()
newDir := filepath.Join(tempDir, "new-dir")
valid, err := isValidDataDir(newDir, true)
require.NoError(t, err)
assert.True(t, valid)
// Verify directory was created
stat, err := os.Stat(newDir)
require.NoError(t, err)
assert.True(t, stat.IsDir())
})
// Test with file instead of directory
t.Run("file instead of directory", func(t *testing.T) {
tempDir := t.TempDir()
tempFile := filepath.Join(tempDir, "testfile")
err := os.WriteFile(tempFile, []byte("test"), 0644)
require.NoError(t, err)
valid, err := isValidDataDir(tempFile, false)
assert.Error(t, err)
assert.False(t, valid)
assert.Contains(t, err.Error(), "is not a directory")
})
}
func TestDirectoryExists(t *testing.T) {
// Test with existing directory
t.Run("existing directory", func(t *testing.T) {
tempDir := t.TempDir()
exists, err := directoryExists(tempDir)
require.NoError(t, err)
assert.True(t, exists)
})
// Test with non-existing directory
t.Run("non-existing directory", func(t *testing.T) {
tempDir := t.TempDir()
nonExistentDir := filepath.Join(tempDir, "does-not-exist")
exists, err := directoryExists(nonExistentDir)
require.NoError(t, err)
assert.False(t, exists)
})
// Test with file instead of directory
t.Run("file instead of directory", func(t *testing.T) {
tempDir := t.TempDir()
tempFile := filepath.Join(tempDir, "testfile")
err := os.WriteFile(tempFile, []byte("test"), 0644)
require.NoError(t, err)
exists, err := directoryExists(tempFile)
assert.Error(t, err)
assert.False(t, exists)
assert.Contains(t, err.Error(), "is not a directory")
})
}
func TestDirectoryIsWritable(t *testing.T) {
// Test with writable directory
t.Run("writable directory", func(t *testing.T) {
tempDir := t.TempDir()
writable, err := directoryIsWritable(tempDir)
require.NoError(t, err)
assert.True(t, writable)
})
// Test with non-existing directory
t.Run("non-existing directory", func(t *testing.T) {
tempDir := t.TempDir()
nonExistentDir := filepath.Join(tempDir, "does-not-exist")
writable, err := directoryIsWritable(nonExistentDir)
assert.Error(t, err)
assert.False(t, writable)
})
// Test with non-writable directory (Unix-like systems only)
t.Run("non-writable directory", func(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("Skipping non-writable directory test on", runtime.GOOS)
}
tempDir := t.TempDir()
readOnlyDir := filepath.Join(tempDir, "readonly")
// Create the directory
err := os.Mkdir(readOnlyDir, 0755)
require.NoError(t, err)
// Make it read-only
err = os.Chmod(readOnlyDir, 0444)
require.NoError(t, err)
// Restore permissions after test for cleanup
defer func() {
os.Chmod(readOnlyDir, 0755)
}()
writable, err := directoryIsWritable(readOnlyDir)
assert.Error(t, err)
assert.False(t, writable)
})
}
================================================
FILE: agent/deltatracker/deltatracker.go
================================================
// Package deltatracker provides a tracker for calculating differences in numeric values over time.
package deltatracker
import (
"sync"
"golang.org/x/exp/constraints"
)
// Numeric is a constraint that permits any integer or floating-point type.
type Numeric interface {
constraints.Integer | constraints.Float
}
// DeltaTracker is a generic, thread-safe tracker for calculating differences
// in numeric values over time.
// K is the key type (e.g., int, string).
// V is the value type (e.g., int, int64, float32, float64).
type DeltaTracker[K comparable, V Numeric] struct {
sync.RWMutex
current map[K]V
previous map[K]V
}
// NewDeltaTracker creates a new generic tracker.
func NewDeltaTracker[K comparable, V Numeric]() *DeltaTracker[K, V] {
return &DeltaTracker[K, V]{
current: make(map[K]V),
previous: make(map[K]V),
}
}
// Set records the current value for a given ID.
func (t *DeltaTracker[K, V]) Set(id K, value V) {
t.Lock()
defer t.Unlock()
t.current[id] = value
}
// Snapshot returns a copy of the current map.
// func (t *DeltaTracker[K, V]) Snapshot() map[K]V {
// t.RLock()
// defer t.RUnlock()
// copyMap := make(map[K]V, len(t.current))
// maps.Copy(copyMap, t.current)
// return copyMap
// }
// Deltas returns a map of all calculated deltas for the current interval.
func (t *DeltaTracker[K, V]) Deltas() map[K]V {
t.RLock()
defer t.RUnlock()
deltas := make(map[K]V)
for id, currentVal := range t.current {
if previousVal, ok := t.previous[id]; ok {
deltas[id] = currentVal - previousVal
} else {
deltas[id] = 0
}
}
return deltas
}
// Previous returns the previously recorded value for the given key, if it exists.
func (t *DeltaTracker[K, V]) Previous(id K) (V, bool) {
t.RLock()
defer t.RUnlock()
value, ok := t.previous[id]
return value, ok
}
// Delta returns the delta for a single key.
// Returns 0 if the key doesn't exist or has no previous value.
func (t *DeltaTracker[K, V]) Delta(id K) V {
t.RLock()
defer t.RUnlock()
currentVal, currentOk := t.current[id]
if !currentOk {
return 0
}
previousVal, previousOk := t.previous[id]
if !previousOk {
return 0
}
return currentVal - previousVal
}
// Cycle prepares the tracker for the next interval.
func (t *DeltaTracker[K, V]) Cycle() {
t.Lock()
defer t.Unlock()
t.previous = t.current
t.current = make(map[K]V)
}
================================================
FILE: agent/deltatracker/deltatracker_test.go
================================================
package deltatracker
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func ExampleDeltaTracker() {
tracker := NewDeltaTracker[string, int]()
tracker.Set("key1", 10)
tracker.Set("key2", 20)
tracker.Cycle()
tracker.Set("key1", 15)
tracker.Set("key2", 30)
fmt.Println(tracker.Delta("key1"))
fmt.Println(tracker.Delta("key2"))
fmt.Println(tracker.Deltas())
// Output: 5
// 10
// map[key1:5 key2:10]
}
func TestNewDeltaTracker(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
assert.NotNil(t, tracker)
assert.Empty(t, tracker.current)
assert.Empty(t, tracker.previous)
}
func TestSet(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
tracker.Set("key1", 10)
tracker.RLock()
defer tracker.RUnlock()
assert.Equal(t, 10, tracker.current["key1"])
}
func TestDeltas(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// Test with no previous values
tracker.Set("key1", 10)
tracker.Set("key2", 20)
deltas := tracker.Deltas()
assert.Equal(t, 0, deltas["key1"])
assert.Equal(t, 0, deltas["key2"])
// Cycle to move current to previous
tracker.Cycle()
// Set new values and check deltas
tracker.Set("key1", 15) // Delta should be 5 (15-10)
tracker.Set("key2", 25) // Delta should be 5 (25-20)
tracker.Set("key3", 30) // New key, delta should be 0
deltas = tracker.Deltas()
assert.Equal(t, 5, deltas["key1"])
assert.Equal(t, 5, deltas["key2"])
assert.Equal(t, 0, deltas["key3"])
}
func TestCycle(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
tracker.Set("key1", 10)
tracker.Set("key2", 20)
// Verify current has values
tracker.RLock()
assert.Equal(t, 10, tracker.current["key1"])
assert.Equal(t, 20, tracker.current["key2"])
assert.Empty(t, tracker.previous)
tracker.RUnlock()
tracker.Cycle()
// After cycle, previous should have the old current values
// and current should be empty
tracker.RLock()
assert.Empty(t, tracker.current)
assert.Equal(t, 10, tracker.previous["key1"])
assert.Equal(t, 20, tracker.previous["key2"])
tracker.RUnlock()
}
func TestCompleteWorkflow(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// First interval
tracker.Set("server1", 100)
tracker.Set("server2", 200)
// Get deltas for first interval (should be zero)
firstDeltas := tracker.Deltas()
assert.Equal(t, 0, firstDeltas["server1"])
assert.Equal(t, 0, firstDeltas["server2"])
// Cycle to next interval
tracker.Cycle()
// Second interval
tracker.Set("server1", 150) // Delta: 50
tracker.Set("server2", 180) // Delta: -20
tracker.Set("server3", 300) // New server, delta: 300
secondDeltas := tracker.Deltas()
assert.Equal(t, 50, secondDeltas["server1"])
assert.Equal(t, -20, secondDeltas["server2"])
assert.Equal(t, 0, secondDeltas["server3"])
}
func TestDeltaTrackerWithDifferentTypes(t *testing.T) {
// Test with int64
intTracker := NewDeltaTracker[string, int64]()
intTracker.Set("pid1", 1000)
intTracker.Cycle()
intTracker.Set("pid1", 1200)
intDeltas := intTracker.Deltas()
assert.Equal(t, int64(200), intDeltas["pid1"])
// Test with float64
floatTracker := NewDeltaTracker[string, float64]()
floatTracker.Set("cpu1", 1.5)
floatTracker.Cycle()
floatTracker.Set("cpu1", 2.7)
floatDeltas := floatTracker.Deltas()
assert.InDelta(t, 1.2, floatDeltas["cpu1"], 0.0001)
// Test with int keys
pidTracker := NewDeltaTracker[int, int64]()
pidTracker.Set(101, 20000)
pidTracker.Cycle()
pidTracker.Set(101, 22500)
pidDeltas := pidTracker.Deltas()
assert.Equal(t, int64(2500), pidDeltas[101])
}
func TestDelta(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// Test getting delta for non-existent key
result := tracker.Delta("nonexistent")
assert.Equal(t, 0, result)
// Test getting delta for key with no previous value
tracker.Set("key1", 10)
result = tracker.Delta("key1")
assert.Equal(t, 0, result)
// Cycle to move current to previous
tracker.Cycle()
// Test getting delta for key with previous value
tracker.Set("key1", 15)
result = tracker.Delta("key1")
assert.Equal(t, 5, result)
// Test getting delta for key that exists in previous but not current
result = tracker.Delta("key1")
assert.Equal(t, 5, result) // Should still return 5
// Test getting delta for key that exists in current but not previous
tracker.Set("key2", 20)
result = tracker.Delta("key2")
assert.Equal(t, 0, result)
}
func TestDeltaWithDifferentTypes(t *testing.T) {
// Test with int64
intTracker := NewDeltaTracker[string, int64]()
intTracker.Set("pid1", 1000)
intTracker.Cycle()
intTracker.Set("pid1", 1200)
result := intTracker.Delta("pid1")
assert.Equal(t, int64(200), result)
// Test with float64
floatTracker := NewDeltaTracker[string, float64]()
floatTracker.Set("cpu1", 1.5)
floatTracker.Cycle()
floatTracker.Set("cpu1", 2.7)
floatResult := floatTracker.Delta("cpu1")
assert.InDelta(t, 1.2, floatResult, 0.0001)
// Test with int keys
pidTracker := NewDeltaTracker[int, int64]()
pidTracker.Set(101, 20000)
pidTracker.Cycle()
pidTracker.Set(101, 22500)
pidResult := pidTracker.Delta(101)
assert.Equal(t, int64(2500), pidResult)
}
func TestDeltaConcurrentAccess(t *testing.T) {
tracker := NewDeltaTracker[string, int]()
// Set initial values
tracker.Set("key1", 10)
tracker.Set("key2", 20)
tracker.Cycle()
// Set new values
tracker.Set("key1", 15)
tracker.Set("key2", 25)
// Test concurrent access safety
result1 := tracker.Delta("key1")
result2 := tracker.Delta("key2")
assert.Equal(t, 5, result1)
assert.Equal(t, 5, result2)
}
================================================
FILE: agent/disk.go
================================================
package agent
import (
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/disk"
)
// fsRegistrationContext holds the shared lookup state needed to resolve a
// filesystem into the tracked fsStats key and metadata.
type fsRegistrationContext struct {
filesystem string // value of optional FILESYSTEM env var
isWindows bool
efPath string // path to extra filesystems (default "/extra-filesystems")
diskIoCounters map[string]disk.IOCountersStat
}
// diskDiscovery groups the transient state for a single initializeDiskInfo run so
// helper methods can share the same partitions, mount paths, and lookup functions
type diskDiscovery struct {
agent *Agent
rootMountPoint string
partitions []disk.PartitionStat
usageFn func(string) (*disk.UsageStat, error)
ctx fsRegistrationContext
}
// parseFilesystemEntry parses a filesystem entry in the format "device__customname"
// Returns the device/filesystem part and the custom name part
func parseFilesystemEntry(entry string) (device, customName string) {
entry = strings.TrimSpace(entry)
if parts := strings.SplitN(entry, "__", 2); len(parts) == 2 {
device = strings.TrimSpace(parts[0])
customName = strings.TrimSpace(parts[1])
} else {
device = entry
}
return device, customName
}
// extraFilesystemPartitionInfo derives the I/O device and optional display name
// for a mounted /extra-filesystems partition. Prefer the partition device reported
// by the system and only use the folder name for custom naming metadata.
func extraFilesystemPartitionInfo(p disk.PartitionStat) (device, customName string) {
device = strings.TrimSpace(p.Device)
folderDevice, customName := parseFilesystemEntry(filepath.Base(p.Mountpoint))
if device == "" {
device = folderDevice
}
return device, customName
}
func isDockerSpecialMountpoint(mountpoint string) bool {
switch mountpoint {
case "/etc/hosts", "/etc/resolv.conf", "/etc/hostname":
return true
}
return false
}
// registerFilesystemStats resolves the tracked key and stats payload for a
// filesystem before it is inserted into fsStats.
func registerFilesystemStats(existing map[string]*system.FsStats, device, mountpoint string, root bool, customName string, ctx fsRegistrationContext) (string, *system.FsStats, bool) {
key := device
if !ctx.isWindows {
key = filepath.Base(device)
}
if root {
// Try to map root device to a diskIoCounters entry. First checks for an
// exact key match, then uses findIoDevice for normalized / prefix-based
// matching (e.g. nda0p2 -> nda0), and finally falls back to FILESYSTEM.
if _, ioMatch := ctx.diskIoCounters[key]; !ioMatch {
if matchedKey, match := findIoDevice(key, ctx.diskIoCounters); match {
key = matchedKey
} else if ctx.filesystem != "" {
if matchedKey, match := findIoDevice(ctx.filesystem, ctx.diskIoCounters); match {
key = matchedKey
}
}
if _, ioMatch = ctx.diskIoCounters[key]; !ioMatch {
slog.Warn("Root I/O unmapped; set FILESYSTEM", "device", device, "mountpoint", mountpoint)
}
}
} else {
// Check if non-root has diskstats and prefer the folder device for
// /extra-filesystems mounts when the discovered partition device is a
// mapper path (e.g. luks UUID) that obscures the underlying block device.
if _, ioMatch := ctx.diskIoCounters[key]; !ioMatch {
if strings.HasPrefix(mountpoint, ctx.efPath) {
folderDevice, _ := parseFilesystemEntry(filepath.Base(mountpoint))
if folderDevice != "" {
if matchedKey, match := findIoDevice(folderDevice, ctx.diskIoCounters); match {
key = matchedKey
}
}
}
if _, ioMatch = ctx.diskIoCounters[key]; !ioMatch {
if matchedKey, match := findIoDevice(key, ctx.diskIoCounters); match {
key = matchedKey
}
}
}
}
if _, exists := existing[key]; exists {
return "", nil, false
}
fsStats := &system.FsStats{Root: root, Mountpoint: mountpoint}
if customName != "" {
fsStats.Name = customName
}
return key, fsStats, true
}
// addFsStat inserts a discovered filesystem if it resolves to a new tracking
// key. The key selection itself lives in buildFsStatRegistration so that logic
// can stay directly unit-tested.
func (d *diskDiscovery) addFsStat(device, mountpoint string, root bool, customName string) {
key, fsStats, ok := registerFilesystemStats(d.agent.fsStats, device, mountpoint, root, customName, d.ctx)
if !ok {
return
}
d.agent.fsStats[key] = fsStats
name := key
if customName != "" {
name = customName
}
slog.Info("Detected disk", "name", name, "device", device, "mount", mountpoint, "io", key, "root", root)
}
// addConfiguredRootFs resolves FILESYSTEM against partitions first, then falls
// back to direct diskstats matching for setups like ZFS where partitions do not
// expose the physical device name.
func (d *diskDiscovery) addConfiguredRootFs() bool {
if d.ctx.filesystem == "" {
return false
}
for _, p := range d.partitions {
if filesystemMatchesPartitionSetting(d.ctx.filesystem, p) {
d.addFsStat(p.Device, p.Mountpoint, true, "")
return true
}
}
// FILESYSTEM may name a physical disk absent from partitions (e.g. ZFS lists
// dataset paths like zroot/ROOT/default, not block devices).
if ioKey, match := findIoDevice(d.ctx.filesystem, d.ctx.diskIoCounters); match {
d.agent.fsStats[ioKey] = &system.FsStats{Root: true, Mountpoint: d.rootMountPoint}
return true
}
slog.Warn("Partition details not found", "filesystem", d.ctx.filesystem)
return false
}
func isRootFallbackPartition(p disk.PartitionStat, rootMountPoint string) bool {
return p.Mountpoint == rootMountPoint ||
(isDockerSpecialMountpoint(p.Mountpoint) && strings.HasPrefix(p.Device, "/dev"))
}
// addPartitionRootFs handles the non-configured root fallback path when a
// partition looks like the active root mount but still needs translating to an
// I/O device key.
func (d *diskDiscovery) addPartitionRootFs(device, mountpoint string) bool {
fs, match := findIoDevice(filepath.Base(device), d.ctx.diskIoCounters)
if !match {
return false
}
// The resolved I/O device is already known here, so use it directly to avoid
// a second fallback search inside buildFsStatRegistration.
d.addFsStat(fs, mountpoint, true, "")
return true
}
// addLastResortRootFs is only used when neither FILESYSTEM nor partition-based
// heuristics can identify root, so it picks the busiest I/O device as a final
// fallback and preserves the root mountpoint for usage collection.
func (d *diskDiscovery) addLastResortRootFs() {
rootKey := mostActiveIoDevice(d.ctx.diskIoCounters)
if rootKey != "" {
slog.Warn("Using most active device for root I/O; set FILESYSTEM to override", "device", rootKey)
} else {
rootKey = filepath.Base(d.rootMountPoint)
if _, exists := d.agent.fsStats[rootKey]; exists {
rootKey = "root"
}
slog.Warn("Root I/O device not detected; set FILESYSTEM to override")
}
d.agent.fsStats[rootKey] = &system.FsStats{Root: true, Mountpoint: d.rootMountPoint}
}
// findPartitionByFilesystemSetting matches an EXTRA_FILESYSTEMS entry against a
// discovered partition either by mountpoint or by device suffix.
func findPartitionByFilesystemSetting(filesystem string, partitions []disk.PartitionStat) (disk.PartitionStat, bool) {
for _, p := range partitions {
if strings.HasSuffix(p.Device, filesystem) || p.Mountpoint == filesystem {
return p, true
}
}
return disk.PartitionStat{}, false
}
// addConfiguredExtraFsEntry resolves one EXTRA_FILESYSTEMS entry, preferring a
// discovered partition and falling back to any path that disk.Usage accepts.
func (d *diskDiscovery) addConfiguredExtraFsEntry(filesystem, customName string) {
if p, found := findPartitionByFilesystemSetting(filesystem, d.partitions); found {
d.addFsStat(p.Device, p.Mountpoint, false, customName)
return
}
if _, err := d.usageFn(filesystem); err == nil {
d.addFsStat(filepath.Base(filesystem), filesystem, false, customName)
return
} else {
slog.Error("Invalid filesystem", "name", filesystem, "err", err)
}
}
// addConfiguredExtraFilesystems parses and registers the comma-separated
// EXTRA_FILESYSTEMS env var entries.
func (d *diskDiscovery) addConfiguredExtraFilesystems(extraFilesystems string) {
for fsEntry := range strings.SplitSeq(extraFilesystems, ",") {
filesystem, customName := parseFilesystemEntry(fsEntry)
d.addConfiguredExtraFsEntry(filesystem, customName)
}
}
// addPartitionExtraFs registers partitions mounted under /extra-filesystems so
// their display names can come from the folder name while their I/O keys still
// prefer the underlying partition device.
func (d *diskDiscovery) addPartitionExtraFs(p disk.PartitionStat) {
if !strings.HasPrefix(p.Mountpoint, d.ctx.efPath) {
return
}
device, customName := extraFilesystemPartitionInfo(p)
d.addFsStat(device, p.Mountpoint, false, customName)
}
// addExtraFilesystemFolders handles bare directories under /extra-filesystems
// that may not appear in partition discovery, while skipping mountpoints that
// were already registered from higher-fidelity sources.
func (d *diskDiscovery) addExtraFilesystemFolders(folderNames []string) {
existingMountpoints := make(map[string]bool, len(d.agent.fsStats))
for _, stats := range d.agent.fsStats {
existingMountpoints[stats.Mountpoint] = true
}
for _, folderName := range folderNames {
mountpoint := filepath.Join(d.ctx.efPath, folderName)
slog.Debug("/extra-filesystems", "mountpoint", mountpoint)
if existingMountpoints[mountpoint] {
continue
}
device, customName := parseFilesystemEntry(folderName)
d.addFsStat(device, mountpoint, false, customName)
}
}
// Sets up the filesystems to monitor for disk usage and I/O.
func (a *Agent) initializeDiskInfo() {
filesystem, _ := utils.GetEnv("FILESYSTEM")
hasRoot := false
isWindows := runtime.GOOS == "windows"
partitions, err := disk.Partitions(false)
if err != nil {
slog.Error("Error getting disk partitions", "err", err)
}
slog.Debug("Disk", "partitions", partitions)
// trim trailing backslash for Windows devices (#1361)
if isWindows {
for i, p := range partitions {
partitions[i].Device = strings.TrimSuffix(p.Device, "\\")
}
}
diskIoCounters, err := disk.IOCounters()
if err != nil {
slog.Error("Error getting diskstats", "err", err)
}
slog.Debug("Disk I/O", "diskstats", diskIoCounters)
ctx := fsRegistrationContext{
filesystem: filesystem,
isWindows: isWindows,
diskIoCounters: diskIoCounters,
efPath: "/extra-filesystems",
}
// Get the appropriate root mount point for this system
discovery := diskDiscovery{
agent: a,
rootMountPoint: a.getRootMountPoint(),
partitions: partitions,
usageFn: disk.Usage,
ctx: ctx,
}
hasRoot = discovery.addConfiguredRootFs()
// Add EXTRA_FILESYSTEMS env var values to fsStats
if extraFilesystems, exists := utils.GetEnv("EXTRA_FILESYSTEMS"); exists {
discovery.addConfiguredExtraFilesystems(extraFilesystems)
}
// Process partitions for various mount points
for _, p := range partitions {
if !hasRoot && isRootFallbackPartition(p, discovery.rootMountPoint) {
hasRoot = discovery.addPartitionRootFs(p.Device, p.Mountpoint)
}
discovery.addPartitionExtraFs(p)
}
// Check all folders in /extra-filesystems and add them if not already present
if folders, err := os.ReadDir(discovery.ctx.efPath); err == nil {
folderNames := make([]string, 0, len(folders))
for _, folder := range folders {
if folder.IsDir() {
folderNames = append(folderNames, folder.Name())
}
}
discovery.addExtraFilesystemFolders(folderNames)
}
// If no root filesystem set, try the most active I/O device as a last
// resort (e.g. ZFS where dataset names are unrelated to disk names).
if !hasRoot {
discovery.addLastResortRootFs()
}
a.pruneDuplicateRootExtraFilesystems()
a.initializeDiskIoStats(diskIoCounters)
}
// Removes extra filesystems that mirror root usage (https://github.com/henrygd/beszel/issues/1428).
func (a *Agent) pruneDuplicateRootExtraFilesystems() {
var rootMountpoint string
for _, stats := range a.fsStats {
if stats != nil && stats.Root {
rootMountpoint = stats.Mountpoint
break
}
}
if rootMountpoint == "" {
return
}
rootUsage, err := disk.Usage(rootMountpoint)
if err != nil {
return
}
for name, stats := range a.fsStats {
if stats == nil || stats.Root {
continue
}
extraUsage, err := disk.Usage(stats.Mountpoint)
if err != nil {
continue
}
if hasSameDiskUsage(rootUsage, extraUsage) {
slog.Info("Ignoring duplicate FS", "name", name, "mount", stats.Mountpoint)
delete(a.fsStats, name)
}
}
}
// hasSameDiskUsage compares root/extra usage with a small byte tolerance.
func hasSameDiskUsage(a, b *disk.UsageStat) bool {
if a == nil || b == nil || a.Total == 0 || b.Total == 0 {
return false
}
// Allow minor drift between sequential disk usage calls.
const toleranceBytes uint64 = 16 * 1024 * 1024
return withinUsageTolerance(a.Total, b.Total, toleranceBytes) &&
withinUsageTolerance(a.Used, b.Used, toleranceBytes)
}
// withinUsageTolerance reports whether two byte values differ by at most tolerance.
func withinUsageTolerance(a, b, tolerance uint64) bool {
if a >= b {
return a-b <= tolerance
}
return b-a <= tolerance
}
type ioMatchCandidate struct {
name string
bytes uint64
ops uint64
}
// findIoDevice prefers exact device/label matches, then falls back to a
// prefix-related candidate with the highest recent activity.
func findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCountersStat) (string, bool) {
filesystem = normalizeDeviceName(filesystem)
if filesystem == "" {
return "", false
}
candidates := []ioMatchCandidate{}
for _, d := range diskIoCounters {
if normalizeDeviceName(d.Name) == filesystem || (d.Label != "" && normalizeDeviceName(d.Label) == filesystem) {
return d.Name, true
}
if prefixRelated(normalizeDeviceName(d.Name), filesystem) ||
(d.Label != "" && prefixRelated(normalizeDeviceName(d.Label), filesystem)) {
candidates = append(candidates, ioMatchCandidate{
name: d.Name,
bytes: d.ReadBytes + d.WriteBytes,
ops: d.ReadCount + d.WriteCount,
})
}
}
if len(candidates) == 0 {
return "", false
}
best := candidates[0]
for _, c := range candidates[1:] {
if c.bytes > best.bytes ||
(c.bytes == best.bytes && c.ops > best.ops) ||
(c.bytes == best.bytes && c.ops == best.ops && c.name < best.name) {
best = c
}
}
slog.Info("Using disk I/O fallback", "requested", filesystem, "selected", best.name)
return best.name, true
}
// mostActiveIoDevice returns the device with the highest I/O activity,
// or "" if diskIoCounters is empty.
func mostActiveIoDevice(diskIoCounters map[string]disk.IOCountersStat) string {
var best ioMatchCandidate
for _, d := range diskIoCounters {
c := ioMatchCandidate{
name: d.Name,
bytes: d.ReadBytes + d.WriteBytes,
ops: d.ReadCount + d.WriteCount,
}
if best.name == "" || c.bytes > best.bytes ||
(c.bytes == best.bytes && c.ops > best.ops) ||
(c.bytes == best.bytes && c.ops == best.ops && c.name < best.name) {
best = c
}
}
return best.name
}
// prefixRelated reports whether either identifier is a prefix of the other.
func prefixRelated(a, b string) bool {
if a == "" || b == "" || a == b {
return false
}
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
}
// filesystemMatchesPartitionSetting checks whether a FILESYSTEM env var value
// matches a partition by mountpoint, exact device name, or prefix relationship
// (e.g. FILESYSTEM=ada0 matches partition /dev/ada0p2).
func filesystemMatchesPartitionSetting(filesystem string, p disk.PartitionStat) bool {
filesystem = strings.TrimSpace(filesystem)
if filesystem == "" {
return false
}
if p.Mountpoint == filesystem {
return true
}
fsName := normalizeDeviceName(filesystem)
partName := normalizeDeviceName(p.Device)
if fsName == "" || partName == "" {
return false
}
if fsName == partName {
return true
}
return prefixRelated(partName, fsName)
}
// normalizeDeviceName canonicalizes device strings for comparisons.
func normalizeDeviceName(value string) string {
name := filepath.Base(strings.TrimSpace(value))
if name == "." {
return ""
}
return name
}
// Sets start values for disk I/O stats.
func (a *Agent) initializeDiskIoStats(diskIoCounters map[string]disk.IOCountersStat) {
a.fsNames = a.fsNames[:0]
now := time.Now()
for device, stats := range a.fsStats {
// skip if not in diskIoCounters
d, exists := diskIoCounters[device]
if !exists {
slog.Warn("Device not found in diskstats", "name", device)
continue
}
// populate initial values
stats.Time = now
stats.TotalRead = d.ReadBytes
stats.TotalWrite = d.WriteBytes
// add to list of valid io device names
a.fsNames = append(a.fsNames, device)
}
}
// Updates disk usage statistics for all monitored filesystems
func (a *Agent) updateDiskUsage(systemStats *system.Stats) {
// Check if we should skip extra filesystem collection to avoid waking sleeping disks.
// Root filesystem is always updated since it can't be sleeping while the agent runs.
// Always collect on first call (lastDiskUsageUpdate is zero) or if caching is disabled.
cacheExtraFs := a.diskUsageCacheDuration > 0 &&
!a.lastDiskUsageUpdate.IsZero() &&
time.Since(a.lastDiskUsageUpdate) < a.diskUsageCacheDuration
// disk usage
for _, stats := range a.fsStats {
// Skip non-root filesystems if caching is active
if cacheExtraFs && !stats.Root {
continue
}
if d, err := disk.Usage(stats.Mountpoint); err == nil {
stats.DiskTotal = utils.BytesToGigabytes(d.Total)
stats.DiskUsed = utils.BytesToGigabytes(d.Used)
if stats.Root {
systemStats.DiskTotal = utils.BytesToGigabytes(d.Total)
systemStats.DiskUsed = utils.BytesToGigabytes(d.Used)
systemStats.DiskPct = utils.TwoDecimals(d.UsedPercent)
}
} else {
// reset stats if error (likely unmounted)
slog.Error("Error getting disk stats", "name", stats.Mountpoint, "err", err)
stats.DiskTotal = 0
stats.DiskUsed = 0
stats.TotalRead = 0
stats.TotalWrite = 0
}
}
// Update the last disk usage update time when we've collected extra filesystems
if !cacheExtraFs {
a.lastDiskUsageUpdate = time.Now()
}
}
// Updates disk I/O statistics for all monitored filesystems
func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
// disk i/o (cache-aware per interval)
if ioCounters, err := disk.IOCounters(a.fsNames...); err == nil {
// Ensure map for this interval exists
if _, ok := a.diskPrev[cacheTimeMs]; !ok {
a.diskPrev[cacheTimeMs] = make(map[string]prevDisk)
}
now := time.Now()
for name, d := range ioCounters {
stats := a.fsStats[d.Name]
if stats == nil {
// skip devices not tracked
continue
}
// Previous snapshot for this interval and device
prev, hasPrev := a.diskPrev[cacheTimeMs][name]
if !hasPrev {
// Seed from agent-level fsStats if present, else seed from current
prev = prevDisk{readBytes: stats.TotalRead, writeBytes: stats.TotalWrite, at: stats.Time}
if prev.at.IsZero() {
prev = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
}
}
msElapsed := uint64(now.Sub(prev.at).Milliseconds())
if msElapsed < 100 {
// Avoid division by zero or clock issues; update snapshot and continue
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
continue
}
diskIORead := (d.ReadBytes - prev.readBytes) * 1000 / msElapsed
diskIOWrite := (d.WriteBytes - prev.writeBytes) * 1000 / msElapsed
readMbPerSecond := utils.BytesToMegabytes(float64(diskIORead))
writeMbPerSecond := utils.BytesToMegabytes(float64(diskIOWrite))
// validate values
if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {
slog.Warn("Invalid disk I/O. Resetting.", "name", d.Name, "read", readMbPerSecond, "write", writeMbPerSecond)
// Reset interval snapshot and seed from current
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
// also refresh agent baseline to avoid future negatives
a.initializeDiskIoStats(ioCounters)
continue
}
// Update per-interval snapshot
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
// Update global fsStats baseline for cross-interval correctness
stats.Time = now
stats.TotalRead = d.ReadBytes
stats.TotalWrite = d.WriteBytes
stats.DiskReadPs = readMbPerSecond
stats.DiskWritePs = writeMbPerSecond
stats.DiskReadBytes = diskIORead
stats.DiskWriteBytes = diskIOWrite
if stats.Root {
systemStats.DiskReadPs = stats.DiskReadPs
systemStats.DiskWritePs = stats.DiskWritePs
systemStats.DiskIO[0] = diskIORead
systemStats.DiskIO[1] = diskIOWrite
}
}
}
}
// getRootMountPoint returns the appropriate root mount point for the system
// For immutable systems like Fedora Silverblue, it returns /sysroot instead of /
func (a *Agent) getRootMountPoint() string {
// 1. Check if /etc/os-release contains indicators of an immutable system
if osReleaseContent, err := os.ReadFile("/etc/os-release"); err == nil {
content := string(osReleaseContent)
if strings.Contains(content, "fedora") && strings.Contains(content, "silverblue") ||
strings.Contains(content, "coreos") ||
strings.Contains(content, "flatcar") ||
strings.Contains(content, "rhel-atomic") ||
strings.Contains(content, "centos-atomic") {
// Verify that /sysroot exists before returning it
if _, err := os.Stat("/sysroot"); err == nil {
return "/sysroot"
}
}
}
// 2. Check if /run/ostree is present (ostree-based systems like Silverblue)
if _, err := os.Stat("/run/ostree"); err == nil {
// Verify that /sysroot exists before returning it
if _, err := os.Stat("/sysroot"); err == nil {
return "/sysroot"
}
}
return "/"
}
================================================
FILE: agent/disk_test.go
================================================
//go:build testing
package agent
import (
"os"
"strings"
"testing"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/disk"
"github.com/stretchr/testify/assert"
)
func TestParseFilesystemEntry(t *testing.T) {
tests := []struct {
name string
input string
expectedFs string
expectedName string
}{
{
name: "simple device name",
input: "sda1",
expectedFs: "sda1",
expectedName: "",
},
{
name: "device with custom name",
input: "sda1__my-storage",
expectedFs: "sda1",
expectedName: "my-storage",
},
{
name: "full device path with custom name",
input: "/dev/sdb1__backup-drive",
expectedFs: "/dev/sdb1",
expectedName: "backup-drive",
},
{
name: "NVMe device with custom name",
input: "nvme0n1p2__fast-ssd",
expectedFs: "nvme0n1p2",
expectedName: "fast-ssd",
},
{
name: "whitespace trimmed",
input: " sda2__trimmed-name ",
expectedFs: "sda2",
expectedName: "trimmed-name",
},
{
name: "empty custom name",
input: "sda3__",
expectedFs: "sda3",
expectedName: "",
},
{
name: "empty device name",
input: "__just-custom",
expectedFs: "",
expectedName: "just-custom",
},
{
name: "multiple underscores in custom name",
input: "sda1__my_custom_drive",
expectedFs: "sda1",
expectedName: "my_custom_drive",
},
{
name: "custom name with spaces",
input: "sda1__My Storage Drive",
expectedFs: "sda1",
expectedName: "My Storage Drive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fsEntry := strings.TrimSpace(tt.input)
var fs, customName string
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
fs = strings.TrimSpace(parts[0])
customName = strings.TrimSpace(parts[1])
} else {
fs = fsEntry
}
assert.Equal(t, tt.expectedFs, fs)
assert.Equal(t, tt.expectedName, customName)
})
}
}
func TestExtraFilesystemPartitionInfo(t *testing.T) {
t.Run("uses partition device for label-only mountpoint", func(t *testing.T) {
device, customName := extraFilesystemPartitionInfo(disk.PartitionStat{
Device: "/dev/sdc",
Mountpoint: "/extra-filesystems/Share",
})
assert.Equal(t, "/dev/sdc", device)
assert.Equal(t, "", customName)
})
t.Run("uses custom name from mountpoint suffix", func(t *testing.T) {
device, customName := extraFilesystemPartitionInfo(disk.PartitionStat{
Device: "/dev/sdc",
Mountpoint: "/extra-filesystems/sdc__Share",
})
assert.Equal(t, "/dev/sdc", device)
assert.Equal(t, "Share", customName)
})
t.Run("falls back to folder device when partition device is unavailable", func(t *testing.T) {
device, customName := extraFilesystemPartitionInfo(disk.PartitionStat{
Mountpoint: "/extra-filesystems/sdc__Share",
})
assert.Equal(t, "sdc", device)
assert.Equal(t, "Share", customName)
})
t.Run("supports custom name without folder device prefix", func(t *testing.T) {
device, customName := extraFilesystemPartitionInfo(disk.PartitionStat{
Device: "/dev/sdc",
Mountpoint: "/extra-filesystems/__Share",
})
assert.Equal(t, "/dev/sdc", device)
assert.Equal(t, "Share", customName)
})
}
func TestBuildFsStatRegistration(t *testing.T) {
t.Run("uses basename for non-windows exact io match", func(t *testing.T) {
key, stats, ok := registerFilesystemStats(
map[string]*system.FsStats{},
"/dev/sda1",
"/mnt/data",
false,
"archive",
fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"sda1": {Name: "sda1"},
},
},
)
assert.True(t, ok)
assert.Equal(t, "sda1", key)
assert.Equal(t, "/mnt/data", stats.Mountpoint)
assert.Equal(t, "archive", stats.Name)
assert.False(t, stats.Root)
})
t.Run("maps root partition to io device by prefix", func(t *testing.T) {
key, stats, ok := registerFilesystemStats(
map[string]*system.FsStats{},
"/dev/ada0p2",
"/",
true,
"",
fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"ada0": {Name: "ada0", ReadBytes: 1000, WriteBytes: 1000},
},
},
)
assert.True(t, ok)
assert.Equal(t, "ada0", key)
assert.True(t, stats.Root)
assert.Equal(t, "/", stats.Mountpoint)
})
t.Run("uses filesystem setting as root fallback", func(t *testing.T) {
key, _, ok := registerFilesystemStats(
map[string]*system.FsStats{},
"overlay",
"/",
true,
"",
fsRegistrationContext{
filesystem: "nvme0n1p2",
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"nvme0n1": {Name: "nvme0n1", ReadBytes: 1000, WriteBytes: 1000},
},
},
)
assert.True(t, ok)
assert.Equal(t, "nvme0n1", key)
})
t.Run("prefers parsed extra-filesystems device over mapper device", func(t *testing.T) {
key, stats, ok := registerFilesystemStats(
map[string]*system.FsStats{},
"/dev/mapper/luks-2bcb02be-999d-4417-8d18-5c61e660fb6e",
"/extra-filesystems/nvme0n1p2__Archive",
false,
"Archive",
fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"dm-1": {Name: "dm-1", Label: "luks-2bcb02be-999d-4417-8d18-5c61e660fb6e"},
"nvme0n1p2": {Name: "nvme0n1p2"},
},
},
)
assert.True(t, ok)
assert.Equal(t, "nvme0n1p2", key)
assert.Equal(t, "Archive", stats.Name)
})
t.Run("falls back to mapper io device when folder device cannot be resolved", func(t *testing.T) {
key, stats, ok := registerFilesystemStats(
map[string]*system.FsStats{},
"/dev/mapper/luks-2bcb02be-999d-4417-8d18-5c61e660fb6e",
"/extra-filesystems/Archive",
false,
"Archive",
fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"dm-1": {Name: "dm-1", Label: "luks-2bcb02be-999d-4417-8d18-5c61e660fb6e"},
},
},
)
assert.True(t, ok)
assert.Equal(t, "dm-1", key)
assert.Equal(t, "Archive", stats.Name)
})
t.Run("uses full device name on windows", func(t *testing.T) {
key, _, ok := registerFilesystemStats(
map[string]*system.FsStats{},
`C:`,
`C:\\`,
false,
"",
fsRegistrationContext{
isWindows: true,
diskIoCounters: map[string]disk.IOCountersStat{
`C:`: {Name: `C:`},
},
},
)
assert.True(t, ok)
assert.Equal(t, `C:`, key)
})
t.Run("skips existing key", func(t *testing.T) {
key, stats, ok := registerFilesystemStats(
map[string]*system.FsStats{"sda1": {Mountpoint: "/existing"}},
"/dev/sda1",
"/mnt/data",
false,
"",
fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"sda1": {Name: "sda1"},
},
},
)
assert.False(t, ok)
assert.Empty(t, key)
assert.Nil(t, stats)
})
}
func TestAddConfiguredRootFs(t *testing.T) {
t.Run("adds root from matching partition", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
rootMountPoint: "/",
partitions: []disk.PartitionStat{{Device: "/dev/ada0p2", Mountpoint: "/"}},
ctx: fsRegistrationContext{
filesystem: "/dev/ada0p2",
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"ada0": {Name: "ada0", ReadBytes: 1000, WriteBytes: 1000},
},
},
}
ok := discovery.addConfiguredRootFs()
assert.True(t, ok)
stats, exists := agent.fsStats["ada0"]
assert.True(t, exists)
assert.True(t, stats.Root)
assert.Equal(t, "/", stats.Mountpoint)
})
t.Run("adds root from io device when partition is missing", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
rootMountPoint: "/sysroot",
ctx: fsRegistrationContext{
filesystem: "zroot",
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"nda0": {Name: "nda0", Label: "zroot", ReadBytes: 1000, WriteBytes: 1000},
},
},
}
ok := discovery.addConfiguredRootFs()
assert.True(t, ok)
stats, exists := agent.fsStats["nda0"]
assert.True(t, exists)
assert.True(t, stats.Root)
assert.Equal(t, "/sysroot", stats.Mountpoint)
})
t.Run("returns false when filesystem cannot be resolved", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
rootMountPoint: "/",
ctx: fsRegistrationContext{
filesystem: "missing-disk",
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{},
},
}
ok := discovery.addConfiguredRootFs()
assert.False(t, ok)
assert.Empty(t, agent.fsStats)
})
}
func TestAddPartitionRootFs(t *testing.T) {
t.Run("adds root from fallback partition candidate", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
ctx: fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"nvme0n1": {Name: "nvme0n1", ReadBytes: 1000, WriteBytes: 1000},
},
},
}
ok := discovery.addPartitionRootFs("/dev/nvme0n1p2", "/")
assert.True(t, ok)
stats, exists := agent.fsStats["nvme0n1"]
assert.True(t, exists)
assert.True(t, stats.Root)
assert.Equal(t, "/", stats.Mountpoint)
})
t.Run("returns false when no io device matches", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{agent: agent, ctx: fsRegistrationContext{diskIoCounters: map[string]disk.IOCountersStat{}}}
ok := discovery.addPartitionRootFs("/dev/mapper/root", "/")
assert.False(t, ok)
assert.Empty(t, agent.fsStats)
})
}
func TestAddLastResortRootFs(t *testing.T) {
t.Run("uses most active io device when available", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{agent: agent, rootMountPoint: "/", ctx: fsRegistrationContext{diskIoCounters: map[string]disk.IOCountersStat{
"sda": {Name: "sda", ReadBytes: 5000, WriteBytes: 5000},
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000},
}}}
discovery.addLastResortRootFs()
stats, exists := agent.fsStats["sda"]
assert.True(t, exists)
assert.True(t, stats.Root)
})
t.Run("falls back to root key when mountpoint basename collides", func(t *testing.T) {
agent := &Agent{fsStats: map[string]*system.FsStats{
"sysroot": {Mountpoint: "/extra-filesystems/sysroot"},
}}
discovery := diskDiscovery{agent: agent, rootMountPoint: "/sysroot", ctx: fsRegistrationContext{diskIoCounters: map[string]disk.IOCountersStat{}}}
discovery.addLastResortRootFs()
stats, exists := agent.fsStats["root"]
assert.True(t, exists)
assert.True(t, stats.Root)
assert.Equal(t, "/sysroot", stats.Mountpoint)
})
}
func TestAddConfiguredExtraFsEntry(t *testing.T) {
t.Run("uses matching partition when present", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
partitions: []disk.PartitionStat{{Device: "/dev/sdb1", Mountpoint: "/mnt/backup"}},
usageFn: func(string) (*disk.UsageStat, error) {
t.Fatal("usage fallback should not be called when partition matches")
return nil, nil
},
ctx: fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"sdb1": {Name: "sdb1"},
},
},
}
discovery.addConfiguredExtraFsEntry("sdb1", "backup")
stats, exists := agent.fsStats["sdb1"]
assert.True(t, exists)
assert.Equal(t, "/mnt/backup", stats.Mountpoint)
assert.Equal(t, "backup", stats.Name)
})
t.Run("falls back to usage-validated path", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
usageFn: func(path string) (*disk.UsageStat, error) {
assert.Equal(t, "/srv/archive", path)
return &disk.UsageStat{}, nil
},
ctx: fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"archive": {Name: "archive"},
},
},
}
discovery.addConfiguredExtraFsEntry("/srv/archive", "archive")
stats, exists := agent.fsStats["archive"]
assert.True(t, exists)
assert.Equal(t, "/srv/archive", stats.Mountpoint)
assert.Equal(t, "archive", stats.Name)
})
t.Run("ignores invalid filesystem entry", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
usageFn: func(string) (*disk.UsageStat, error) {
return nil, os.ErrNotExist
},
}
discovery.addConfiguredExtraFsEntry("/missing/archive", "")
assert.Empty(t, agent.fsStats)
})
}
func TestAddConfiguredExtraFilesystems(t *testing.T) {
t.Run("parses and registers multiple configured filesystems", func(t *testing.T) {
agent := &Agent{fsStats: make(map[string]*system.FsStats)}
discovery := diskDiscovery{
agent: agent,
partitions: []disk.PartitionStat{{Device: "/dev/sda1", Mountpoint: "/mnt/fast"}},
usageFn: func(path string) (*disk.UsageStat, error) {
if path == "/srv/archive" {
return &disk.UsageStat{}, nil
}
return nil, os.ErrNotExist
},
ctx: fsRegistrationContext{
isWindows: false,
diskIoCounters: map[string]disk.IOCountersStat{
"sda1": {Name: "sda1"},
"archive": {Name: "archive"},
},
},
}
discovery.addConfiguredExtraFilesystems("sda1__fast,/srv/archive__cold")
assert.Contains(t, agent.fsStats, "sda1")
assert.Equal(t, "fast", agent.fsStats["sda1"].Name)
assert.Contains(t, agent.fsStats, "archive")
assert.Equal(t, "cold", agent.fsStats["archive"].Name)
})
}
func TestAddExtraFilesystemFolders(t *testing.T) {
t.Run("adds missing folders and skips existing mountpoints", func(t *testing.T) {
agent := &Agent{fsStats: map[string]*system.FsStats{
"existing": {Mountpoint: "/extra-filesystems/existing"},
}}
discovery := diskDiscovery{
agent: agent,
ctx: fsRegistrationContext{
isWindows: false,
efPath: "/extra-filesystems",
diskIoCounters: map[string]disk.IOCountersStat{
"newdisk": {Name: "newdisk"},
},
},
}
discovery.addExtraFilesystemFolders([]string{"existing", "newdisk__Archive"})
assert.Len(t, agent.fsStats, 2)
stats, exists := agent.fsStats["newdisk"]
assert.True(t, exists)
assert.Equal(t, "/extra-filesystems/newdisk__Archive", stats.Mountpoint)
assert.Equal(t, "Archive", stats.Name)
})
}
func TestFindIoDevice(t *testing.T) {
t.Run("matches by device name", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda"},
"sdb": {Name: "sdb"},
}
device, ok := findIoDevice("sdb", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sdb", device)
})
t.Run("matches by device label", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda", Label: "rootfs"},
"sdb": {Name: "sdb"},
}
device, ok := findIoDevice("rootfs", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
t.Run("returns no match when not found", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda"},
"sdb": {Name: "sdb"},
}
device, ok := findIoDevice("nvme0n1p1", ioCounters)
assert.False(t, ok)
assert.Equal(t, "", device)
})
t.Run("uses uncertain unique prefix fallback", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"nvme0n1": {Name: "nvme0n1"},
"sda": {Name: "sda"},
}
device, ok := findIoDevice("nvme0n1p2", ioCounters)
assert.True(t, ok)
assert.Equal(t, "nvme0n1", device)
})
t.Run("uses dominant activity when prefix matches are ambiguous", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},
}
device, ok := findIoDevice("sd", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
t.Run("uses highest activity when ambiguous without dominance", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sda": {Name: "sda", ReadBytes: 3000, WriteBytes: 3000, ReadCount: 50, WriteCount: 50},
"sdb": {Name: "sdb", ReadBytes: 2500, WriteBytes: 2500, ReadCount: 40, WriteCount: 40},
}
device, ok := findIoDevice("sd", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
t.Run("matches /dev/-prefixed partition to parent disk", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"nda0": {Name: "nda0", ReadBytes: 1000, WriteBytes: 1000},
}
device, ok := findIoDevice("/dev/nda0p2", ioCounters)
assert.True(t, ok)
assert.Equal(t, "nda0", device)
})
t.Run("uses deterministic name tie-breaker", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sdb": {Name: "sdb", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},
"sda": {Name: "sda", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},
}
device, ok := findIoDevice("sd", ioCounters)
assert.True(t, ok)
assert.Equal(t, "sda", device)
})
}
func TestFilesystemMatchesPartitionSetting(t *testing.T) {
p := disk.PartitionStat{Device: "/dev/ada0p2", Mountpoint: "/"}
t.Run("matches mountpoint setting", func(t *testing.T) {
assert.True(t, filesystemMatchesPartitionSetting("/", p))
})
t.Run("matches exact partition setting", func(t *testing.T) {
assert.True(t, filesystemMatchesPartitionSetting("ada0p2", p))
assert.True(t, filesystemMatchesPartitionSetting("/dev/ada0p2", p))
})
t.Run("matches prefix-style parent setting", func(t *testing.T) {
assert.True(t, filesystemMatchesPartitionSetting("ada0", p))
assert.True(t, filesystemMatchesPartitionSetting("/dev/ada0", p))
})
t.Run("does not match unrelated device", func(t *testing.T) {
assert.False(t, filesystemMatchesPartitionSetting("sda", p))
assert.False(t, filesystemMatchesPartitionSetting("nvme0n1", p))
assert.False(t, filesystemMatchesPartitionSetting("", p))
})
}
func TestMostActiveIoDevice(t *testing.T) {
t.Run("returns most active device", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"nda0": {Name: "nda0", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},
"nda1": {Name: "nda1", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},
}
assert.Equal(t, "nda0", mostActiveIoDevice(ioCounters))
})
t.Run("uses deterministic tie-breaker", func(t *testing.T) {
ioCounters := map[string]disk.IOCountersStat{
"sdb": {Name: "sdb", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},
"sda": {Name: "sda", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},
}
assert.Equal(t, "sda", mostActiveIoDevice(ioCounters))
})
t.Run("returns empty for empty map", func(t *testing.T) {
assert.Equal(t, "", mostActiveIoDevice(map[string]disk.IOCountersStat{}))
})
}
func TestIsDockerSpecialMountpoint(t *testing.T) {
testCases := []struct {
name string
mountpoint string
expected bool
}{
{name: "hosts", mountpoint: "/etc/hosts", expected: true},
{name: "resolv", mountpoint: "/etc/resolv.conf", expected: true},
{name: "hostname", mountpoint: "/etc/hostname", expected: true},
{name: "root", mountpoint: "/", expected: false},
{name: "passwd", mountpoint: "/etc/passwd", expected: false},
{name: "extra-filesystem", mountpoint: "/extra-filesystems/sda1", expected: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, isDockerSpecialMountpoint(tc.mountpoint))
})
}
}
func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
// Set up environment variables
oldEnv := os.Getenv("EXTRA_FILESYSTEMS")
defer func() {
if oldEnv != "" {
os.Setenv("EXTRA_FILESYSTEMS", oldEnv)
} else {
os.Unsetenv("EXTRA_FILESYSTEMS")
}
}()
// Test with custom names
os.Setenv("EXTRA_FILESYSTEMS", "sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2")
// Mock disk partitions (we'll just test the parsing logic)
// Since the actual disk operations are system-dependent, we'll focus on the parsing
testCases := []struct {
envValue string
expectedFs []string
expectedNames map[string]string
}{
{
envValue: "sda1__my-storage,sdb1__backup-drive",
expectedFs: []string{"sda1", "sdb1"},
expectedNames: map[string]string{
"sda1": "my-storage",
"sdb1": "backup-drive",
},
},
{
envValue: "sda1,nvme0n1p2__fast-ssd",
expectedFs: []string{"sda1", "nvme0n1p2"},
expectedNames: map[string]string{
"nvme0n1p2": "fast-ssd",
},
},
}
for _, tc := range testCases {
t.Run("env_"+tc.envValue, func(t *testing.T) {
os.Setenv("EXTRA_FILESYSTEMS", tc.envValue)
// Create mock partitions that would match our test cases
partitions := []disk.PartitionStat{}
for _, fs := range tc.expectedFs {
if strings.HasPrefix(fs, "/dev/") {
partitions = append(partitions, disk.PartitionStat{
Device: fs,
Mountpoint: fs,
})
} else {
partitions = append(partitions, disk.PartitionStat{
Device: "/dev/" + fs,
Mountpoint: "/" + fs,
})
}
}
// Test the parsing logic by calling the relevant part
// We'll create a simplified version to test just the parsing
extraFilesystems := tc.envValue
for fsEntry := range strings.SplitSeq(extraFilesystems, ",") {
// Parse the entry
fsEntry = strings.TrimSpace(fsEntry)
var fs, customName string
if parts := strings.SplitN(fsEntry, "__", 2); len(parts) == 2 {
fs = strings.TrimSpace(parts[0])
customName = strings.TrimSpace(parts[1])
} else {
fs = fsEntry
}
// Verify the device is in our expected list
assert.Contains(t, tc.expectedFs, fs, "parsed device should be in expected list")
// Check if custom name should exist
if expectedName, exists := tc.expectedNames[fs]; exists {
assert.Equal(t, expectedName, customName, "custom name should match expected")
} else {
assert.Empty(t, customName, "custom name should be empty when not expected")
}
}
})
}
}
func TestFsStatsWithCustomNames(t *testing.T) {
// Test that FsStats properly stores custom names
fsStats := &system.FsStats{
Mountpoint: "/mnt/storage",
Name: "my-custom-storage",
DiskTotal: 100.0,
DiskUsed: 50.0,
}
assert.Equal(t, "my-custom-storage", fsStats.Name)
assert.Equal(t, "/mnt/storage", fsStats.Mountpoint)
assert.Equal(t, 100.0, fsStats.DiskTotal)
assert.Equal(t, 50.0, fsStats.DiskUsed)
}
func TestExtraFsKeyGeneration(t *testing.T) {
// Test the logic for generating ExtraFs keys with custom names
testCases := []struct {
name string
deviceName string
customName string
expectedKey string
}{
{
name: "with custom name",
deviceName: "sda1",
customName: "my-storage",
expectedKey: "my-storage",
},
{
name: "without custom name",
deviceName: "sda1",
customName: "",
expectedKey: "sda1",
},
{
name: "empty custom name falls back to device",
deviceName: "nvme0n1p2",
customName: "",
expectedKey: "nvme0n1p2",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Simulate the key generation logic from agent.go
key := tc.deviceName
if tc.customName != "" {
key = tc.customName
}
assert.Equal(t, tc.expectedKey, key)
})
}
}
func TestDiskUsageCaching(t *testing.T) {
t.Run("caching disabled updates all filesystems", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/"},
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
},
diskUsageCacheDuration: 0, // caching disabled
}
var stats system.Stats
agent.updateDiskUsage(&stats)
// Both should be updated (non-zero values from disk.Usage)
// Root stats should be populated in systemStats
assert.True(t, agent.lastDiskUsageUpdate.IsZero() || !agent.lastDiskUsageUpdate.IsZero(),
"lastDiskUsageUpdate should be set when caching is disabled")
})
t.Run("caching enabled always updates root filesystem", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/", DiskTotal: 100, DiskUsed: 50},
"sdb": {Root: false, Mountpoint: "/mnt/storage", DiskTotal: 200, DiskUsed: 100},
},
diskUsageCacheDuration: 1 * time.Hour,
lastDiskUsageUpdate: time.Now(), // cache is fresh
}
// Store original extra fs values
originalExtraTotal := agent.fsStats["sdb"].DiskTotal
originalExtraUsed := agent.fsStats["sdb"].DiskUsed
var stats system.Stats
agent.updateDiskUsage(&stats)
// Root should be updated (systemStats populated from disk.Usage call)
// We can't easily check if disk.Usage was called, but we verify the flow works
// Extra filesystem should retain cached values (not reset)
assert.Equal(t, originalExtraTotal, agent.fsStats["sdb"].DiskTotal,
"extra filesystem DiskTotal should be unchanged when cached")
assert.Equal(t, originalExtraUsed, agent.fsStats["sdb"].DiskUsed,
"extra filesystem DiskUsed should be unchanged when cached")
})
t.Run("first call always updates all filesystems", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/"},
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
},
diskUsageCacheDuration: 1 * time.Hour,
// lastDiskUsageUpdate is zero (first call)
}
var stats system.Stats
agent.updateDiskUsage(&stats)
// After first call, lastDiskUsageUpdate should be set
assert.False(t, agent.lastDiskUsageUpdate.IsZero(),
"lastDiskUsageUpdate should be set after first call")
})
t.Run("expired cache updates extra filesystems", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/"},
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
},
diskUsageCacheDuration: 1 * time.Millisecond,
lastDiskUsageUpdate: time.Now().Add(-1 * time.Second), // cache expired
}
var stats system.Stats
agent.updateDiskUsage(&stats)
// lastDiskUsageUpdate should be refreshed since cache expired
assert.True(t, time.Since(agent.lastDiskUsageUpdate) < time.Second,
"lastDiskUsageUpdate should be refreshed when cache expires")
})
}
func TestHasSameDiskUsage(t *testing.T) {
const toleranceBytes uint64 = 16 * 1024 * 1024
t.Run("returns true when totals and usage are equal", func(t *testing.T) {
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
b := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
assert.True(t, hasSameDiskUsage(a, b))
})
t.Run("returns true within tolerance", func(t *testing.T) {
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
b := &disk.UsageStat{
Total: a.Total + toleranceBytes - 1,
Used: a.Used - toleranceBytes + 1,
}
assert.True(t, hasSameDiskUsage(a, b))
})
t.Run("returns false when total exceeds tolerance", func(t *testing.T) {
a := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}
b := &disk.UsageStat{
Total: a.Total + toleranceBytes + 1,
Used: a.Used,
}
assert.False(t, hasSameDiskUsage(a, b))
})
t.Run("returns false for nil or zero total", func(t *testing.T) {
assert.False(t, hasSameDiskUsage(nil, &disk.UsageStat{Total: 1, Used: 1}))
assert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 1, Used: 1}, nil))
assert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 0, Used: 0}, &disk.UsageStat{Total: 1, Used: 1}))
})
}
func TestInitializeDiskIoStatsResetsTrackedDevices(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {},
"sdb": {},
},
fsNames: []string{"stale", "sda"},
}
agent.initializeDiskIoStats(map[string]disk.IOCountersStat{
"sda": {Name: "sda", ReadBytes: 10, WriteBytes: 20},
"sdb": {Name: "sdb", ReadBytes: 30, WriteBytes: 40},
})
assert.ElementsMatch(t, []string{"sda", "sdb"}, agent.fsNames)
assert.Len(t, agent.fsNames, 2)
assert.Equal(t, uint64(10), agent.fsStats["sda"].TotalRead)
assert.Equal(t, uint64(20), agent.fsStats["sda"].TotalWrite)
assert.False(t, agent.fsStats["sda"].Time.IsZero())
assert.False(t, agent.fsStats["sdb"].Time.IsZero())
agent.initializeDiskIoStats(map[string]disk.IOCountersStat{
"sdb": {Name: "sdb", ReadBytes: 50, WriteBytes: 60},
})
assert.Equal(t, []string{"sdb"}, agent.fsNames)
assert.Equal(t, uint64(50), agent.fsStats["sdb"].TotalRead)
assert.Equal(t, uint64(60), agent.fsStats["sdb"].TotalWrite)
}
================================================
FILE: agent/docker.go
================================================
package agent
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/agent/utils"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/blang/semver"
)
// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.)
// This includes CSI sequences like \x1b[...m and simple escapes like \x1b[K
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`)
var dockerContainerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
const (
// Docker API timeout in milliseconds
dockerTimeoutMs = 2100
// Maximum realistic network speed (5 GB/s) to detect bad deltas
maxNetworkSpeedBps uint64 = 5e9
// Maximum conceivable memory usage of a container (100TB) to detect bad memory stats
maxMemoryUsage uint64 = 100 * 1024 * 1024 * 1024 * 1024
// Number of log lines to request when fetching container logs
dockerLogsTail = 200
// Maximum size of a single log frame (1MB) to prevent memory exhaustion
// A single log line larger than 1MB is likely an error or misconfiguration
maxLogFrameSize = 1024 * 1024
// Maximum total log content size (5MB) to prevent memory exhaustion
// This provides a reasonable limit for network transfer and browser rendering
maxTotalLogSize = 5 * 1024 * 1024
)
type dockerManager struct {
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList []*container.ApiInfo // List of containers from Docker API
containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
// Maps cache time intervals to container-specific CPU usage tracking
lastCpuContainer map[uint16]map[string]uint64 // cacheTimeMs -> containerId -> last cpu container usage
lastCpuSystem map[uint16]map[string]uint64 // cacheTimeMs -> containerId -> last cpu system usage
lastCpuReadTime map[uint16]map[string]time.Time // cacheTimeMs -> containerId -> last read time (Windows)
// Network delta trackers - one per cache time to avoid interference
// cacheTimeMs -> DeltaTracker for network bytes sent/received
networkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
networkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
retrySleep func(time.Duration)
}
// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests
type userAgentRoundTripper struct {
rt http.RoundTripper
userAgent string
}
// RoundTrip implements the http.RoundTripper interface
func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", u.userAgent)
return u.rt.RoundTrip(req)
}
// Add goroutine to the queue
func (d *dockerManager) queue() {
d.wg.Add(1)
if d.goodDockerVersion {
d.sem <- struct{}{}
}
}
// Remove goroutine from the queue
func (d *dockerManager) dequeue() {
d.wg.Done()
if d.goodDockerVersion {
<-d.sem
}
}
// shouldExcludeContainer checks if a container name matches any exclusion pattern
func (dm *dockerManager) shouldExcludeContainer(name string) bool {
if len(dm.excludeContainers) == 0 {
return false
}
for _, pattern := range dm.excludeContainers {
if match, _ := path.Match(pattern, name); match {
return true
}
}
return false
}
// Returns stats for all running containers with cache-time-aware delta tracking
func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats, error) {
resp, err := dm.client.Get("http://localhost/containers/json")
if err != nil {
return nil, err
}
dm.apiContainerList = dm.apiContainerList[:0]
if err := dm.decode(resp, &dm.apiContainerList); err != nil {
return nil, err
}
dm.isWindows = strings.Contains(resp.Header.Get("Server"), "windows")
containersLength := len(dm.apiContainerList)
// store valid ids to clean up old container ids from map
if dm.validIds == nil {
dm.validIds = make(map[string]struct{}, containersLength)
} else {
clear(dm.validIds)
}
var failedContainers []*container.ApiInfo
for _, ctr := range dm.apiContainerList {
ctr.IdShort = ctr.Id[:12]
// Skip this container if it matches the exclusion pattern
if dm.shouldExcludeContainer(ctr.Names[0][1:]) {
slog.Debug("Excluding container", "name", ctr.Names[0][1:])
continue
}
dm.validIds[ctr.IdShort] = struct{}{}
// check if container is less than 1 minute old (possible restart)
// note: can't use Created field because it's not updated on restart
if strings.Contains(ctr.Status, "second") {
// if so, remove old container data
dm.deleteContainerStatsSync(ctr.IdShort)
}
dm.queue()
go func(ctr *container.ApiInfo) {
defer dm.dequeue()
err := dm.updateContainerStats(ctr, cacheTimeMs)
// if error, delete from map and add to failed list to retry
if err != nil {
dm.containerStatsMutex.Lock()
delete(dm.containerStatsMap, ctr.IdShort)
failedContainers = append(failedContainers, ctr)
dm.containerStatsMutex.Unlock()
}
}(ctr)
}
dm.wg.Wait()
// retry failed containers separately so we can run them in parallel (docker 24 bug)
if len(failedContainers) > 0 {
slog.Debug("Retrying failed containers", "count", len(failedContainers))
for i := range failedContainers {
ctr := failedContainers[i]
dm.queue()
go func(ctr *container.ApiInfo) {
defer dm.dequeue()
if err2 := dm.updateContainerStats(ctr, cacheTimeMs); err2 != nil {
slog.Error("Error getting container stats", "err", err2)
}
}(ctr)
}
dm.wg.Wait()
}
// populate final stats and remove old / invalid container stats
stats := make([]*container.Stats, 0, containersLength)
for id, v := range dm.containerStatsMap {
if _, exists := dm.validIds[id]; !exists {
delete(dm.containerStatsMap, id)
} else {
stats = append(stats, v)
}
}
// prepare network trackers for next interval for this cache time
dm.cycleNetworkDeltasForCacheTime(cacheTimeMs)
return stats, nil
}
// initializeCpuTracking initializes CPU tracking maps for a specific cache time interval
func (dm *dockerManager) initializeCpuTracking(cacheTimeMs uint16) {
// Initialize cache time maps if they don't exist
if dm.lastCpuContainer[cacheTimeMs] == nil {
dm.lastCpuContainer[cacheTimeMs] = make(map[string]uint64)
}
if dm.lastCpuSystem[cacheTimeMs] == nil {
dm.lastCpuSystem[cacheTimeMs] = make(map[string]uint64)
}
// Ensure the outer map exists before indexing
if dm.lastCpuReadTime == nil {
dm.lastCpuReadTime = make(map[uint16]map[string]time.Time)
}
if dm.lastCpuReadTime[cacheTimeMs] == nil {
dm.lastCpuReadTime[cacheTimeMs] = make(map[string]time.Time)
}
}
// getCpuPreviousValues returns previous CPU values for a container and cache time interval
func (dm *dockerManager) getCpuPreviousValues(cacheTimeMs uint16, containerId string) (uint64, uint64) {
return dm.lastCpuContainer[cacheTimeMs][containerId], dm.lastCpuSystem[cacheTimeMs][containerId]
}
// setCpuCurrentValues stores current CPU values for a container and cache time interval
func (dm *dockerManager) setCpuCurrentValues(cacheTimeMs uint16, containerId string, cpuContainer, cpuSystem uint64) {
dm.lastCpuContainer[cacheTimeMs][containerId] = cpuContainer
dm.lastCpuSystem[cacheTimeMs][containerId] = cpuSystem
}
// calculateMemoryUsage calculates memory usage from Docker API stats
func calculateMemoryUsage(apiStats *container.ApiStats, isWindows bool) (uint64, error) {
if isWindows {
return apiStats.MemoryStats.PrivateWorkingSet, nil
}
memCache := apiStats.MemoryStats.Stats.InactiveFile
if memCache == 0 {
memCache = apiStats.MemoryStats.Stats.Cache
}
usedDelta := apiStats.MemoryStats.Usage - memCache
if usedDelta <= 0 || usedDelta > maxMemoryUsage {
return 0, fmt.Errorf("bad memory stats")
}
return usedDelta, nil
}
// getNetworkTracker returns the DeltaTracker for a specific cache time, creating it if needed
func (dm *dockerManager) getNetworkTracker(cacheTimeMs uint16, isSent bool) *deltatracker.DeltaTracker[string, uint64] {
var trackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
if isSent {
trackers = dm.networkSentTrackers
} else {
trackers = dm.networkRecvTrackers
}
if trackers[cacheTimeMs] == nil {
trackers[cacheTimeMs] = deltatracker.NewDeltaTracker[string, uint64]()
}
return trackers[cacheTimeMs]
}
// cycleNetworkDeltasForCacheTime cycles the network delta trackers for a specific cache time
func (dm *dockerManager) cycleNetworkDeltasForCacheTime(cacheTimeMs uint16) {
if dm.networkSentTrackers[cacheTimeMs] != nil {
dm.networkSentTrackers[cacheTimeMs].Cycle()
}
if dm.networkRecvTrackers[cacheTimeMs] != nil {
dm.networkRecvTrackers[cacheTimeMs].Cycle()
}
}
// calculateNetworkStats calculates network sent/receive deltas using DeltaTracker
func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats *container.ApiStats, stats *container.Stats, initialized bool, name string, cacheTimeMs uint16) (uint64, uint64) {
var total_sent, total_recv uint64
for _, v := range apiStats.Networks {
total_sent += v.TxBytes
total_recv += v.RxBytes
}
// Get the DeltaTracker for this specific cache time
sentTracker := dm.getNetworkTracker(cacheTimeMs, true)
recvTracker := dm.getNetworkTracker(cacheTimeMs, false)
// Set current values in the cache-time-specific DeltaTracker
sentTracker.Set(ctr.IdShort, total_sent)
recvTracker.Set(ctr.IdShort, total_recv)
// Get deltas (bytes since last measurement)
sent_delta_raw := sentTracker.Delta(ctr.IdShort)
recv_delta_raw := recvTracker.Delta(ctr.IdShort)
// Calculate bytes per second independently for Tx and Rx if we have previous data
var sent_delta, recv_delta uint64
if initialized {
millisecondsElapsed := uint64(time.Since(stats.PrevReadTime).Milliseconds())
if millisecondsElapsed > 0 {
if sent_delta_raw > 0 {
sent_delta = sent_delta_raw * 1000 / millisecondsElapsed
if sent_delta > maxNetworkSpeedBps {
slog.Warn("Bad network delta", "container", name)
sent_delta = 0
}
}
if recv_delta_raw > 0 {
recv_delta = recv_delta_raw * 1000 / millisecondsElapsed
if recv_delta > maxNetworkSpeedBps {
slog.Warn("Bad network delta", "container", name)
recv_delta = 0
}
}
}
}
return sent_delta, recv_delta
}
// validateCpuPercentage checks if CPU percentage is within valid range
func validateCpuPercentage(cpuPct float64, containerName string) error {
if cpuPct > 100 {
return fmt.Errorf("%s cpu pct greater than 100: %+v", containerName, cpuPct)
}
return nil
}
// updateContainerStatsValues updates the final stats values
func updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemory uint64, sent_delta, recv_delta uint64, readTime time.Time) {
stats.Cpu = utils.TwoDecimals(cpuPct)
stats.Mem = utils.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 = utils.BytesToMegabytes(float64(sent_delta))
stats.NetworkRecv = utils.BytesToMegabytes(float64(recv_delta))
stats.PrevReadTime = readTime
}
// convertContainerPortsToString formats the ports of a container into a sorted, deduplicated string.
// ctr.Ports is nilled out after processing so the slice is not accidentally reused.
func convertContainerPortsToString(ctr *container.ApiInfo) string {
if len(ctr.Ports) == 0 {
return ""
}
sort.Slice(ctr.Ports, func(i, j int) bool {
return ctr.Ports[i].PublicPort < ctr.Ports[j].PublicPort
})
var builder strings.Builder
seenPorts := make(map[uint16]struct{})
for _, p := range ctr.Ports {
_, ok := seenPorts[p.PublicPort]
if p.PublicPort == 0 || ok {
continue
}
seenPorts[p.PublicPort] = struct{}{}
if builder.Len() > 0 {
builder.WriteString(", ")
}
switch p.IP {
case "0.0.0.0", "::":
default:
builder.WriteString(p.IP)
builder.WriteByte(':')
}
builder.WriteString(strconv.Itoa(int(p.PublicPort)))
}
// clear ports slice so it doesn't get reused and blend into next response
ctr.Ports = nil
return builder.String()
}
func parseDockerStatus(status string) (string, container.DockerHealth) {
trimmed := strings.TrimSpace(status)
if trimmed == "" {
return "", container.DockerHealthNone
}
// Remove "About " from status
trimmed = strings.Replace(trimmed, "About ", "", 1)
openIdx := strings.LastIndex(trimmed, "(")
if openIdx == -1 || !strings.HasSuffix(trimmed, ")") {
return trimmed, container.DockerHealthNone
}
statusText := strings.TrimSpace(trimmed[:openIdx])
if statusText == "" {
statusText = trimmed
}
healthText := strings.TrimSpace(strings.TrimSuffix(trimmed[openIdx+1:], ")"))
// Some Docker statuses include a "health:" prefix inside the parentheses.
// Strip it so it maps correctly to the known health states.
if colonIdx := strings.IndexRune(healthText, ':'); colonIdx != -1 {
prefix := strings.ToLower(strings.TrimSpace(healthText[:colonIdx]))
if prefix == "health" || prefix == "health status" {
healthText = strings.TrimSpace(healthText[colonIdx+1:])
}
}
if health, ok := parseDockerHealthStatus(healthText); ok {
return statusText, health
}
return trimmed, container.DockerHealthNone
}
// parseDockerHealthStatus maps Docker health status strings to container.DockerHealth values
func parseDockerHealthStatus(status string) (container.DockerHealth, bool) {
health, ok := container.DockerHealthStrings[strings.ToLower(strings.TrimSpace(status))]
return health, ok
}
// getPodmanContainerHealth fetches container health status from the container inspect endpoint.
// Used for Podman which doesn't provide health status in the /containers/json endpoint as of March 2026.
// https://github.com/containers/podman/issues/27786
func (dm *dockerManager) getPodmanContainerHealth(containerID string) (container.DockerHealth, error) {
resp, err := dm.client.Get(fmt.Sprintf("http://localhost/containers/%s/json", url.PathEscape(containerID)))
if err != nil {
return container.DockerHealthNone, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return container.DockerHealthNone, fmt.Errorf("container inspect request failed: %s", resp.Status)
}
var inspectInfo struct {
State struct {
Health struct {
Status string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&inspectInfo); err != nil {
return container.DockerHealthNone, err
}
if health, ok := parseDockerHealthStatus(inspectInfo.State.Health.Status); ok {
return health, nil
}
return container.DockerHealthNone, nil
}
// Updates stats for individual container with cache-time-aware delta tracking
func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeMs uint16) error {
name := ctr.Names[0][1:]
resp, err := dm.client.Get(fmt.Sprintf("http://localhost/containers/%s/stats?stream=0&one-shot=1", ctr.IdShort))
if err != nil {
return err
}
statusText, health := parseDockerStatus(ctr.Status)
// Docker exposes Health.Status on /containers/json in API 1.52+.
// Podman currently requires falling back to the inspect endpoint as of March 2026.
// https://github.com/containers/podman/issues/27786
if ctr.Health.Status != "" {
if h, ok := parseDockerHealthStatus(ctr.Health.Status); ok {
health = h
}
} else if dm.usingPodman {
if podmanHealth, err := dm.getPodmanContainerHealth(ctr.IdShort); err == nil {
health = podmanHealth
}
}
dm.containerStatsMutex.Lock()
defer dm.containerStatsMutex.Unlock()
// add empty values if they doesn't exist in map
stats, initialized := dm.containerStatsMap[ctr.IdShort]
if !initialized {
stats = &container.Stats{Name: name, Id: ctr.IdShort, Image: ctr.Image}
dm.containerStatsMap[ctr.IdShort] = stats
}
stats.Id = ctr.IdShort
stats.Status = statusText
stats.Health = health
if len(ctr.Ports) > 0 {
stats.Ports = convertContainerPortsToString(ctr)
}
// 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
res := dm.apiStats
res.Networks = nil
if err := dm.decode(resp, res); err != nil {
return err
}
// Initialize CPU tracking for this cache time interval
dm.initializeCpuTracking(cacheTimeMs)
// Get previous CPU values
prevCpuContainer, prevCpuSystem := dm.getCpuPreviousValues(cacheTimeMs, ctr.IdShort)
// Calculate CPU percentage based on platform
var cpuPct float64
if dm.isWindows {
prevRead := dm.lastCpuReadTime[cacheTimeMs][ctr.IdShort]
cpuPct = res.CalculateCpuPercentWindows(prevCpuContainer, prevRead)
} else {
cpuPct = res.CalculateCpuPercentLinux(prevCpuContainer, prevCpuSystem)
}
// Calculate memory usage
usedMemory, err := calculateMemoryUsage(res, dm.isWindows)
if err != nil {
return fmt.Errorf("%s - %w - see https://github.com/henrygd/beszel/issues/144", name, err)
}
// Store current CPU stats for next calculation
currentCpuContainer := res.CPUStats.CPUUsage.TotalUsage
currentCpuSystem := res.CPUStats.SystemUsage
dm.setCpuCurrentValues(cacheTimeMs, ctr.IdShort, currentCpuContainer, currentCpuSystem)
// Validate CPU percentage
if err := validateCpuPercentage(cpuPct, name); err != nil {
return err
}
// Calculate network stats using DeltaTracker
sent_delta, recv_delta := dm.calculateNetworkStats(ctr, res, stats, initialized, name, cacheTimeMs)
// Store current network values for legacy compatibility
var total_sent, total_recv uint64
for _, v := range res.Networks {
total_sent += v.TxBytes
total_recv += v.RxBytes
}
stats.PrevNet.Sent, stats.PrevNet.Recv = total_sent, total_recv
// Update final stats values
updateContainerStatsValues(stats, cpuPct, usedMemory, sent_delta, recv_delta, res.Read)
// store per-cache-time read time for Windows CPU percent calc
dm.lastCpuReadTime[cacheTimeMs][ctr.IdShort] = res.Read
return nil
}
// Delete container stats from map using mutex
func (dm *dockerManager) deleteContainerStatsSync(id string) {
dm.containerStatsMutex.Lock()
defer dm.containerStatsMutex.Unlock()
delete(dm.containerStatsMap, id)
for ct := range dm.lastCpuContainer {
delete(dm.lastCpuContainer[ct], id)
}
for ct := range dm.lastCpuSystem {
delete(dm.lastCpuSystem[ct], id)
}
for ct := range dm.lastCpuReadTime {
delete(dm.lastCpuReadTime[ct], id)
}
}
// Creates a new http client for Docker or Podman API
func newDockerManager() *dockerManager {
dockerHost, exists := utils.GetEnv("DOCKER_HOST")
if exists {
// return nil if set to empty string
if dockerHost == "" {
return nil
}
} else {
dockerHost = getDockerHost()
}
parsedURL, err := url.Parse(dockerHost)
if err != nil {
os.Exit(1)
}
transport := &http.Transport{
DisableCompression: true,
MaxConnsPerHost: 0,
}
switch parsedURL.Scheme {
case "unix":
transport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", parsedURL.Path)
}
case "tcp", "http", "https":
transport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "tcp", parsedURL.Host)
}
default:
slog.Error("Invalid DOCKER_HOST", "scheme", parsedURL.Scheme)
os.Exit(1)
}
// configurable timeout
timeout := time.Millisecond * time.Duration(dockerTimeoutMs)
if t, set := utils.GetEnv("DOCKER_TIMEOUT"); set {
timeout, err = time.ParseDuration(t)
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
slog.Info("DOCKER_TIMEOUT", "timeout", timeout)
}
// Custom user-agent to avoid docker bug: https://github.com/docker/for-mac/issues/7575
userAgentTransport := &userAgentRoundTripper{
rt: transport,
userAgent: "Docker-Client/",
}
// Read container exclusion patterns from environment variable
var excludeContainers []string
if excludeStr, set := utils.GetEnv("EXCLUDE_CONTAINERS"); set && excludeStr != "" {
parts := strings.SplitSeq(excludeStr, ",")
for part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
excludeContainers = append(excludeContainers, trimmed)
}
}
slog.Info("EXCLUDE_CONTAINERS", "patterns", excludeContainers)
}
manager := &dockerManager{
client: &http.Client{
Timeout: timeout,
Transport: userAgentTransport,
},
containerStatsMap: make(map[string]*container.Stats),
sem: make(chan struct{}, 5),
apiContainerList: []*container.ApiInfo{},
apiStats: &container.ApiStats{},
excludeContainers: excludeContainers,
// Initialize cache-time-aware tracking structures
lastCpuContainer: make(map[uint16]map[string]uint64),
lastCpuSystem: make(map[uint16]map[string]uint64),
lastCpuReadTime: make(map[uint16]map[string]time.Time),
networkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
networkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),
retrySleep: time.Sleep,
}
// If using podman, return client
if strings.Contains(dockerHost, "podman") {
manager.usingPodman = true
manager.goodDockerVersion = true
return manager
}
// run version check in goroutine to avoid blocking (server may not be ready and requires retries)
go manager.checkDockerVersion()
// give version check a chance to complete before returning
time.Sleep(50 * time.Millisecond)
return manager
}
// checkDockerVersion checks Docker version and sets goodDockerVersion if at least 25.0.0.
// Versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch.
func (dm *dockerManager) checkDockerVersion() {
var err error
var resp *http.Response
var versionInfo struct {
Version string `json:"Version"`
}
const versionMaxTries = 2
for i := 1; i <= versionMaxTries; i++ {
resp, err = dm.client.Get("http://localhost/version")
if err == nil && resp.StatusCode == http.StatusOK {
break
}
if resp != nil {
resp.Body.Close()
}
if i < versionMaxTries {
slog.Debug("Failed to get Docker version; retrying", "attempt", i, "err", err, "response", resp)
dm.retrySleep(5 * time.Second)
}
}
if err != nil || resp.StatusCode != http.StatusOK {
return
}
if err := dm.decode(resp, &versionInfo); err != nil {
return
}
// if version > 24, one-shot works correctly and we can limit concurrent operations
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
dm.goodDockerVersion = true
} else {
slog.Info(fmt.Sprintf("Docker %s is outdated. Upgrade if possible. See https://github.com/henrygd/beszel/issues/58", versionInfo.Version))
}
}
// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe.
func (dm *dockerManager) decode(resp *http.Response, d any) error {
if dm.buf == nil {
// initialize buffer with 256kb starting size
dm.buf = bytes.NewBuffer(make([]byte, 0, 1024*256))
dm.decoder = json.NewDecoder(dm.buf)
}
defer resp.Body.Close()
defer dm.buf.Reset()
_, err := dm.buf.ReadFrom(resp.Body)
if err != nil {
return err
}
return dm.decoder.Decode(d)
}
// Test docker / podman sockets and return if one exists
func getDockerHost() string {
scheme := "unix://"
socks := []string{"/var/run/docker.sock", fmt.Sprintf("/run/user/%v/podman/podman.sock", os.Getuid())}
for _, sock := range socks {
if _, err := os.Stat(sock); err == nil {
return scheme + sock
}
}
return scheme + socks[0]
}
func validateContainerID(containerID string) error {
if !dockerContainerIDPattern.MatchString(containerID) {
return fmt.Errorf("invalid container id")
}
return nil
}
func buildDockerContainerEndpoint(containerID, action string, query url.Values) (string, erro
gitextract_xj546sdv/
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── DISCUSSION_TEMPLATE/
│ │ ├── ideas.yml
│ │ └── support.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── funding.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── docker-images.yml
│ ├── inactivity-actions.yml
│ ├── release.yml
│ └── vulncheck.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── Makefile
├── SECURITY.md
├── agent/
│ ├── agent.go
│ ├── agent_cache.go
│ ├── agent_cache_test.go
│ ├── agent_test_helpers.go
│ ├── battery/
│ │ ├── battery.go
│ │ └── battery_freebsd.go
│ ├── client.go
│ ├── client_test.go
│ ├── connection_manager.go
│ ├── connection_manager_test.go
│ ├── cpu.go
│ ├── data_dir.go
│ ├── data_dir_test.go
│ ├── deltatracker/
│ │ ├── deltatracker.go
│ │ └── deltatracker_test.go
│ ├── disk.go
│ ├── disk_test.go
│ ├── docker.go
│ ├── docker_test.go
│ ├── emmc_common.go
│ ├── emmc_common_test.go
│ ├── emmc_linux.go
│ ├── emmc_linux_test.go
│ ├── emmc_stub.go
│ ├── fingerprint.go
│ ├── fingerprint_test.go
│ ├── gpu.go
│ ├── gpu_amd_linux.go
│ ├── gpu_amd_linux_test.go
│ ├── gpu_amd_unsupported.go
│ ├── gpu_darwin.go
│ ├── gpu_darwin_test.go
│ ├── gpu_darwin_unsupported.go
│ ├── gpu_intel.go
│ ├── gpu_nvml.go
│ ├── gpu_nvml_linux.go
│ ├── gpu_nvml_unsupported.go
│ ├── gpu_nvml_windows.go
│ ├── gpu_nvtop.go
│ ├── gpu_test.go
│ ├── handlers.go
│ ├── handlers_test.go
│ ├── health/
│ │ ├── health.go
│ │ └── health_test.go
│ ├── lhm/
│ │ ├── beszel_lhm.cs
│ │ └── beszel_lhm.csproj
│ ├── mdraid_linux.go
│ ├── mdraid_linux_test.go
│ ├── mdraid_stub.go
│ ├── network.go
│ ├── network_test.go
│ ├── response.go
│ ├── sensors.go
│ ├── sensors_default.go
│ ├── sensors_test.go
│ ├── sensors_windows.go
│ ├── server.go
│ ├── server_test.go
│ ├── smart.go
│ ├── smart_nonwindows.go
│ ├── smart_test.go
│ ├── smart_windows.go
│ ├── system.go
│ ├── systemd.go
│ ├── systemd_nonlinux.go
│ ├── systemd_nonlinux_test.go
│ ├── systemd_test.go
│ ├── test-data/
│ │ ├── amdgpu.ids
│ │ ├── container.json
│ │ ├── container2.json
│ │ ├── nvtop.json
│ │ ├── smart/
│ │ │ ├── nvme0.json
│ │ │ ├── scan.json
│ │ │ ├── scsi.json
│ │ │ └── sda.json
│ │ └── system_info.json
│ ├── tools/
│ │ └── fetchsmartctl/
│ │ └── main.go
│ ├── update.go
│ ├── utils/
│ │ ├── utils.go
│ │ └── utils_test.go
│ └── zfs/
│ ├── zfs_freebsd.go
│ ├── zfs_linux.go
│ └── zfs_unsupported.go
├── beszel.go
├── go.mod
├── go.sum
├── i18n.yml
├── internal/
│ ├── alerts/
│ │ ├── alerts.go
│ │ ├── alerts_api.go
│ │ ├── alerts_battery_test.go
│ │ ├── alerts_cache.go
│ │ ├── alerts_cache_test.go
│ │ ├── alerts_disk_test.go
│ │ ├── alerts_history.go
│ │ ├── alerts_quiet_hours_test.go
│ │ ├── alerts_smart.go
│ │ ├── alerts_smart_test.go
│ │ ├── alerts_status.go
│ │ ├── alerts_status_test.go
│ │ ├── alerts_system.go
│ │ ├── alerts_system_test.go
│ │ ├── alerts_test.go
│ │ └── alerts_test_helpers.go
│ ├── cmd/
│ │ ├── agent/
│ │ │ ├── agent.go
│ │ │ └── agent_test.go
│ │ └── hub/
│ │ └── hub.go
│ ├── common/
│ │ ├── common-ssh.go
│ │ └── common-ws.go
│ ├── dockerfile_agent
│ ├── dockerfile_agent_alpine
│ ├── dockerfile_agent_intel
│ ├── dockerfile_agent_nvidia
│ ├── dockerfile_hub
│ ├── entities/
│ │ ├── container/
│ │ │ └── container.go
│ │ ├── smart/
│ │ │ ├── smart.go
│ │ │ └── smart_test.go
│ │ ├── system/
│ │ │ └── system.go
│ │ └── systemd/
│ │ ├── systemd.go
│ │ └── systemd_test.go
│ ├── ghupdate/
│ │ ├── extract.go
│ │ ├── ghupdate.go
│ │ ├── ghupdate_test.go
│ │ ├── release.go
│ │ ├── selinux.go
│ │ └── selinux_test.go
│ ├── hub/
│ │ ├── agent_connect.go
│ │ ├── agent_connect_test.go
│ │ ├── config/
│ │ │ ├── config.go
│ │ │ └── config_test.go
│ │ ├── expirymap/
│ │ │ ├── expirymap.go
│ │ │ └── expirymap_test.go
│ │ ├── heartbeat/
│ │ │ ├── heartbeat.go
│ │ │ └── heartbeat_test.go
│ │ ├── hub.go
│ │ ├── hub_test.go
│ │ ├── hub_test_helpers.go
│ │ ├── server_development.go
│ │ ├── server_production.go
│ │ ├── systems/
│ │ │ ├── system.go
│ │ │ ├── system_manager.go
│ │ │ ├── system_realtime.go
│ │ │ ├── system_smart.go
│ │ │ ├── system_systemd_test.go
│ │ │ ├── system_test.go
│ │ │ ├── systems_production.go
│ │ │ ├── systems_test.go
│ │ │ └── systems_test_helpers.go
│ │ ├── transport/
│ │ │ ├── ssh.go
│ │ │ ├── transport.go
│ │ │ └── websocket.go
│ │ ├── update.go
│ │ └── ws/
│ │ ├── handlers.go
│ │ ├── request_manager.go
│ │ ├── request_manager_test.go
│ │ ├── ws.go
│ │ ├── ws_test.go
│ │ └── ws_test_helpers.go
│ ├── migrations/
│ │ ├── 0_collections_snapshot_0_19_0_dev_1.go
│ │ └── initial-settings.go
│ ├── records/
│ │ ├── records.go
│ │ ├── records_test.go
│ │ └── records_test_helpers.go
│ ├── site/
│ │ ├── .gitignore
│ │ ├── .prettierrc
│ │ ├── biome.json
│ │ ├── components.json
│ │ ├── embed.go
│ │ ├── index.html
│ │ ├── lingui.config.ts
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── static/
│ │ │ └── manifest.json
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── active-alerts.tsx
│ │ │ │ ├── add-system.tsx
│ │ │ │ ├── alerts/
│ │ │ │ │ ├── alert-button.tsx
│ │ │ │ │ └── alerts-sheet.tsx
│ │ │ │ ├── alerts-history-columns.tsx
│ │ │ │ ├── charts/
│ │ │ │ │ ├── area-chart.tsx
│ │ │ │ │ ├── chart-time-select.tsx
│ │ │ │ │ ├── container-chart.tsx
│ │ │ │ │ ├── disk-chart.tsx
│ │ │ │ │ ├── gpu-power-chart.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── line-chart.tsx
│ │ │ │ │ ├── load-average-chart.tsx
│ │ │ │ │ ├── mem-chart.tsx
│ │ │ │ │ ├── swap-chart.tsx
│ │ │ │ │ └── temperature-chart.tsx
│ │ │ │ ├── command-palette.tsx
│ │ │ │ ├── containers-table/
│ │ │ │ │ ├── containers-table-columns.tsx
│ │ │ │ │ └── containers-table.tsx
│ │ │ │ ├── copy-to-clipboard.tsx
│ │ │ │ ├── footer-repo-link.tsx
│ │ │ │ ├── install-dropdowns.tsx
│ │ │ │ ├── lang-toggle.tsx
│ │ │ │ ├── login/
│ │ │ │ │ ├── auth-form.tsx
│ │ │ │ │ ├── forgot-pass-form.tsx
│ │ │ │ │ ├── login.tsx
│ │ │ │ │ └── otp-forms.tsx
│ │ │ │ ├── logo.tsx
│ │ │ │ ├── mode-toggle.tsx
│ │ │ │ ├── navbar.tsx
│ │ │ │ ├── router.tsx
│ │ │ │ ├── routes/
│ │ │ │ │ ├── containers.tsx
│ │ │ │ │ ├── home.tsx
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── alerts-history-data-table.tsx
│ │ │ │ │ │ ├── config-yaml.tsx
│ │ │ │ │ │ ├── general.tsx
│ │ │ │ │ │ ├── heartbeat.tsx
│ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ ├── notifications.tsx
│ │ │ │ │ │ ├── quiet-hours.tsx
│ │ │ │ │ │ ├── sidebar-nav.tsx
│ │ │ │ │ │ └── tokens-fingerprints.tsx
│ │ │ │ │ ├── smart.tsx
│ │ │ │ │ ├── system/
│ │ │ │ │ │ ├── cpu-sheet.tsx
│ │ │ │ │ │ ├── info-bar.tsx
│ │ │ │ │ │ ├── network-sheet.tsx
│ │ │ │ │ │ └── smart-table.tsx
│ │ │ │ │ └── system.tsx
│ │ │ │ ├── spinner.tsx
│ │ │ │ ├── systemd-table/
│ │ │ │ │ ├── systemd-table-columns.tsx
│ │ │ │ │ └── systemd-table.tsx
│ │ │ │ ├── systems-table/
│ │ │ │ │ ├── systems-table-columns.tsx
│ │ │ │ │ └── systems-table.tsx
│ │ │ │ ├── theme-provider.tsx
│ │ │ │ └── ui/
│ │ │ │ ├── alert-dialog.tsx
│ │ │ │ ├── alert.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── chart.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── icons.tsx
│ │ │ │ ├── input-copy.tsx
│ │ │ │ ├── input-tags.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── otp.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── slider.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── toast.tsx
│ │ │ │ ├── toaster.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ └── use-toast.ts
│ │ │ ├── index.css
│ │ │ ├── lib/
│ │ │ │ ├── alerts.ts
│ │ │ │ ├── api.ts
│ │ │ │ ├── enums.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── languages.ts
│ │ │ │ ├── shiki.ts
│ │ │ │ ├── stores.ts
│ │ │ │ ├── systemsManager.ts
│ │ │ │ ├── time.ts
│ │ │ │ ├── use-intersection-observer.ts
│ │ │ │ └── utils.ts
│ │ │ ├── locales/
│ │ │ │ ├── ar/
│ │ │ │ │ └── ar.po
│ │ │ │ ├── bg/
│ │ │ │ │ └── bg.po
│ │ │ │ ├── cs/
│ │ │ │ │ └── cs.po
│ │ │ │ ├── da/
│ │ │ │ │ └── da.po
│ │ │ │ ├── de/
│ │ │ │ │ └── de.po
│ │ │ │ ├── en/
│ │ │ │ │ └── en.po
│ │ │ │ ├── es/
│ │ │ │ │ └── es.po
│ │ │ │ ├── fa/
│ │ │ │ │ └── fa.po
│ │ │ │ ├── fr/
│ │ │ │ │ └── fr.po
│ │ │ │ ├── he/
│ │ │ │ │ └── he.po
│ │ │ │ ├── hr/
│ │ │ │ │ └── hr.po
│ │ │ │ ├── hu/
│ │ │ │ │ └── hu.po
│ │ │ │ ├── id/
│ │ │ │ │ └── id.po
│ │ │ │ ├── it/
│ │ │ │ │ └── it.po
│ │ │ │ ├── ja/
│ │ │ │ │ └── ja.po
│ │ │ │ ├── ko/
│ │ │ │ │ └── ko.po
│ │ │ │ ├── nl/
│ │ │ │ │ └── nl.po
│ │ │ │ ├── no/
│ │ │ │ │ └── no.po
│ │ │ │ ├── pl/
│ │ │ │ │ └── pl.po
│ │ │ │ ├── pt/
│ │ │ │ │ └── pt.po
│ │ │ │ ├── ru/
│ │ │ │ │ └── ru.po
│ │ │ │ ├── sl/
│ │ │ │ │ └── sl.po
│ │ │ │ ├── sr/
│ │ │ │ │ └── sr.po
│ │ │ │ ├── sv/
│ │ │ │ │ └── sv.po
│ │ │ │ ├── tr/
│ │ │ │ │ └── tr.po
│ │ │ │ ├── uk/
│ │ │ │ │ └── uk.po
│ │ │ │ ├── vi/
│ │ │ │ │ └── vi.po
│ │ │ │ ├── zh/
│ │ │ │ │ └── zh.po
│ │ │ │ ├── zh-CN/
│ │ │ │ │ └── zh-CN.po
│ │ │ │ └── zh-HK/
│ │ │ │ └── zh-HK.po
│ │ │ ├── main.tsx
│ │ │ ├── types.d.ts
│ │ │ └── vite-env.d.ts
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── tests/
│ │ ├── api.go
│ │ └── hub.go
│ └── users/
│ └── users.go
├── readme.md
└── supplemental/
├── CHANGELOG.md
├── debian/
│ ├── beszel-agent.service
│ ├── config.sh
│ ├── copyright
│ ├── lintian-overrides
│ ├── postinstall.sh
│ ├── postrm.sh
│ ├── prerm.sh
│ └── templates
├── docker/
│ ├── agent/
│ │ └── docker-compose.yml
│ ├── hub/
│ │ └── docker-compose.yml
│ └── same-system/
│ └── docker-compose.yml
├── guides/
│ └── systemd.md
├── kubernetes/
│ └── beszel-hub/
│ └── charts/
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates/
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ ├── service.yaml
│ │ ├── tests/
│ │ │ └── test-beszel-hub-endpoint.yaml
│ │ └── volume-claim.yaml
│ └── values.yaml
├── licenses/
│ ├── LibreHardwareMonitor/
│ │ └── LICENSE
│ └── smartmontools/
│ └── LICENSE
└── scripts/
├── install-agent-brew.sh
├── install-agent.ps1
├── install-agent.sh
├── install-hub.sh
├── upgrade-agent-wrapper.ps1
└── upgrade-agent.ps1
SYMBOL INDEX (1605 symbols across 206 files)
FILE: agent/agent.go
type Agent (line 22) | type Agent struct
method gatherStats (line 151) | func (a *Agent) gatherStats(options common.DataRequestOptions) *system...
method Start (line 219) | func (a *Agent) Start(serverOptions ServerOptions) error {
method getFingerprint (line 224) | func (a *Agent) getFingerprint() string {
function NewAgent (line 52) | func NewAgent(dataDir ...string) (agent *Agent, err error) {
FILE: agent/agent_cache.go
type systemDataCache (line 10) | type systemDataCache struct
method Get (line 28) | func (c *systemDataCache) Get(cacheTimeMs uint16) (stats *system.Combi...
method Set (line 44) | func (c *systemDataCache) Set(data *system.CombinedData, cacheTimeMs u...
type cacheNode (line 15) | type cacheNode struct
function NewSystemDataCache (line 21) | func NewSystemDataCache() *systemDataCache {
FILE: agent/agent_cache_test.go
function createTestCacheData (line 16) | func createTestCacheData() *system.CombinedData {
function TestNewSystemDataCache (line 35) | func TestNewSystemDataCache(t *testing.T) {
function TestCacheGetSet (line 42) | func TestCacheGetSet(t *testing.T) {
function TestCacheFreshness (line 59) | func TestCacheFreshness(t *testing.T) {
function TestCacheMultipleIntervals (line 120) | func TestCacheMultipleIntervals(t *testing.T) {
function TestCacheOverwrite (line 164) | func TestCacheOverwrite(t *testing.T) {
function TestCacheMiss (line 192) | func TestCacheMiss(t *testing.T) {
function TestCacheZeroInterval (line 215) | func TestCacheZeroInterval(t *testing.T) {
function TestCacheLargeInterval (line 228) | func TestCacheLargeInterval(t *testing.T) {
FILE: agent/agent_test_helpers.go
method GetConnectionManager (line 6) | func (a *Agent) GetConnectionManager() *ConnectionManager {
FILE: agent/battery/battery.go
function HasReadableBattery (line 20) | func HasReadableBattery() bool {
function GetBatteryStats (line 40) | func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err er...
FILE: agent/battery/battery_freebsd.go
function HasReadableBattery (line 7) | func HasReadableBattery() bool {
function GetBatteryStats (line 11) | func GetBatteryStats() (uint8, uint8, error) {
FILE: agent/client.go
constant wsDeadline (line 26) | wsDeadline = 70 * time.Second
type WebSocketClient (line 31) | type WebSocketClient struct
method getOptions (line 94) | func (client *WebSocketClient) getOptions() *gws.ClientOption {
method Connect (line 121) | func (client *WebSocketClient) Connect() (err error) {
method OnOpen (line 139) | func (client *WebSocketClient) OnOpen(conn *gws.Conn) {
method OnClose (line 145) | func (client *WebSocketClient) OnClose(conn *gws.Conn, err error) {
method OnMessage (line 154) | func (client *WebSocketClient) OnMessage(conn *gws.Conn, message *gws....
method OnPing (line 177) | func (client *WebSocketClient) OnPing(conn *gws.Conn, message []byte) {
method handleAuthChallenge (line 183) | func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequ...
method verifySignature (line 211) | func (client *WebSocketClient) verifySignature(signature []byte) (err ...
method Close (line 226) | func (client *WebSocketClient) Close() {
method handleHubRequest (line 233) | func (client *WebSocketClient) handleHubRequest(msg *common.HubRequest...
method sendMessage (line 246) | func (client *WebSocketClient) sendMessage(data any) error {
method sendResponse (line 263) | func (client *WebSocketClient) sendResponse(data any, requestID *uint3...
function newWebSocketClient (line 46) | func newWebSocketClient(agent *Agent) (client *WebSocketClient, err erro...
function getToken (line 74) | func getToken() (string, error) {
function getUserAgent (line 274) | func getUserAgent() string {
FILE: agent/client_test.go
function TestNewWebSocketClient (line 24) | func TestNewWebSocketClient(t *testing.T) {
function TestWebSocketClient_GetOptions (line 109) | func TestWebSocketClient_GetOptions(t *testing.T) {
function TestWebSocketClient_VerifySignature (line 173) | func TestWebSocketClient_VerifySignature(t *testing.T) {
function TestWebSocketClient_HandleHubRequest (line 257) | func TestWebSocketClient_HandleHubRequest(t *testing.T) {
function TestGetUserAgent (line 322) | func TestGetUserAgent(t *testing.T) {
function TestWebSocketClient_Close (line 350) | func TestWebSocketClient_Close(t *testing.T) {
function TestWebSocketClient_ConnectRateLimit (line 371) | func TestWebSocketClient_ConnectRateLimit(t *testing.T) {
function TestGetToken (line 395) | func TestGetToken(t *testing.T) {
FILE: agent/connection_manager.go
type ConnectionManager (line 18) | type ConnectionManager struct
method startWsTicker (line 62) | func (c *ConnectionManager) startWsTicker() {
method stopWsTicker (line 71) | func (c *ConnectionManager) stopWsTicker() {
method Start (line 79) | func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
method handleEvent (line 122) | func (c *ConnectionManager) handleEvent(event ConnectionEvent) {
method handleStateChange (line 141) | func (c *ConnectionManager) handleStateChange(newState ConnectionState) {
method connect (line 176) | func (c *ConnectionManager) connect() {
method startWebSocketConnection (line 195) | func (c *ConnectionManager) startWebSocketConnection() error {
method startSSHServer (line 215) | func (c *ConnectionManager) startSSHServer() {
method closeWebSocket (line 222) | func (c *ConnectionManager) closeWebSocket() {
type ConnectionState (line 30) | type ConnectionState
type ConnectionEvent (line 33) | type ConnectionEvent
constant Disconnected (line 37) | Disconnected ConnectionState = iota
constant WebSocketConnected (line 38) | WebSocketConnected
constant SSHConnected (line 39) | SSHConnected
constant WebSocketConnect (line 44) | WebSocketConnect ConnectionEvent = iota
constant WebSocketDisconnect (line 45) | WebSocketDisconnect
constant SSHConnect (line 46) | SSHConnect
constant SSHDisconnect (line 47) | SSHDisconnect
constant wsTickerInterval (line 50) | wsTickerInterval = 10 * time.Second
function newConnectionManager (line 53) | func newConnectionManager(agent *Agent) *ConnectionManager {
FILE: agent/connection_manager_test.go
function createTestAgent (line 19) | func createTestAgent(t *testing.T) *Agent {
function createTestServerOptions (line 26) | func createTestServerOptions(t *testing.T) ServerOptions {
function TestConnectionManager_NewConnectionManager (line 47) | func TestConnectionManager_NewConnectionManager(t *testing.T) {
function TestConnectionManager_StateTransitions (line 61) | func TestConnectionManager_StateTransitions(t *testing.T) {
function TestConnectionManager_EventHandling (line 90) | func TestConnectionManager_EventHandling(t *testing.T) {
function TestConnectionManager_TickerManagement (line 153) | func TestConnectionManager_TickerManagement(t *testing.T) {
function TestConnectionManager_WebSocketConnectionFlow (line 185) | func TestConnectionManager_WebSocketConnectionFlow(t *testing.T) {
function TestConnectionManager_ReconnectionLogic (line 215) | func TestConnectionManager_ReconnectionLogic(t *testing.T) {
function TestConnectionManager_ConnectWithRateLimit (line 232) | func TestConnectionManager_ConnectWithRateLimit(t *testing.T) {
function TestConnectionManager_StartWithInvalidConfig (line 266) | func TestConnectionManager_StartWithInvalidConfig(t *testing.T) {
function TestConnectionManager_CloseWebSocket (line 278) | func TestConnectionManager_CloseWebSocket(t *testing.T) {
function TestConnectionManager_ConnectFlow (line 306) | func TestConnectionManager_ConnectFlow(t *testing.T) {
FILE: agent/cpu.go
function init (line 16) | func init() {
type CpuMetrics (line 26) | type CpuMetrics struct
function getCpuMetrics (line 37) | func getCpuMetrics(cacheTimeMs uint16) (CpuMetrics, error) {
function clampPercent (line 72) | func clampPercent(value float64) float64 {
function getPerCoreCpuUsage (line 78) | func getPerCoreCpuUsage(cacheTimeMs uint16) (system.Uint8Slice, error) {
function calculateBusy (line 108) | func calculateBusy(t1, t2 cpu.TimesStat) float64 {
function getAllBusy (line 121) | func getAllBusy(t cpu.TimesStat) (float64, float64) {
FILE: agent/data_dir.go
function GetDataDir (line 16) | func GetDataDir(dataDirs ...string) (string, error) {
function testDataDirs (line 40) | func testDataDirs(paths []string) (string, error) {
function isValidDataDir (line 70) | func isValidDataDir(path string, createIfNotExists bool) (bool, error) {
function directoryExists (line 94) | func directoryExists(path string) (bool, error) {
function directoryIsWritable (line 110) | func directoryIsWritable(path string) (bool, error) {
FILE: agent/data_dir_test.go
function TestGetDataDir (line 15) | func TestGetDataDir(t *testing.T) {
function TestTestDataDirs (line 90) | func TestTestDataDirs(t *testing.T) {
function TestIsValidDataDir (line 140) | func TestIsValidDataDir(t *testing.T) {
function TestDirectoryExists (line 186) | func TestDirectoryExists(t *testing.T) {
function TestDirectoryIsWritable (line 218) | func TestDirectoryIsWritable(t *testing.T) {
FILE: agent/deltatracker/deltatracker.go
type Numeric (line 11) | type Numeric interface
type DeltaTracker (line 19) | type DeltaTracker struct
function NewDeltaTracker (line 26) | func NewDeltaTracker[K comparable, V Numeric]() *DeltaTracker[K, V] {
method Set (line 34) | func (t *DeltaTracker[K, V]) Set(id K, value V) {
method Deltas (line 51) | func (t *DeltaTracker[K, V]) Deltas() map[K]V {
method Previous (line 67) | func (t *DeltaTracker[K, V]) Previous(id K) (V, bool) {
method Delta (line 77) | func (t *DeltaTracker[K, V]) Delta(id K) V {
method Cycle (line 95) | func (t *DeltaTracker[K, V]) Cycle() {
FILE: agent/deltatracker/deltatracker_test.go
function ExampleDeltaTracker (line 10) | func ExampleDeltaTracker() {
function TestNewDeltaTracker (line 25) | func TestNewDeltaTracker(t *testing.T) {
function TestSet (line 32) | func TestSet(t *testing.T) {
function TestDeltas (line 42) | func TestDeltas(t *testing.T) {
function TestCycle (line 67) | func TestCycle(t *testing.T) {
function TestCompleteWorkflow (line 91) | func TestCompleteWorkflow(t *testing.T) {
function TestDeltaTrackerWithDifferentTypes (line 117) | func TestDeltaTrackerWithDifferentTypes(t *testing.T) {
function TestDelta (line 143) | func TestDelta(t *testing.T) {
function TestDeltaWithDifferentTypes (line 173) | func TestDeltaWithDifferentTypes(t *testing.T) {
function TestDeltaConcurrentAccess (line 199) | func TestDeltaConcurrentAccess(t *testing.T) {
FILE: agent/disk.go
type fsRegistrationContext (line 19) | type fsRegistrationContext struct
type diskDiscovery (line 28) | type diskDiscovery struct
method addFsStat (line 128) | func (d *diskDiscovery) addFsStat(device, mountpoint string, root bool...
method addConfiguredRootFs (line 144) | func (d *diskDiscovery) addConfiguredRootFs() bool {
method addPartitionRootFs (line 175) | func (d *diskDiscovery) addPartitionRootFs(device, mountpoint string) ...
method addLastResortRootFs (line 189) | func (d *diskDiscovery) addLastResortRootFs() {
method addConfiguredExtraFsEntry (line 216) | func (d *diskDiscovery) addConfiguredExtraFsEntry(filesystem, customNa...
method addConfiguredExtraFilesystems (line 232) | func (d *diskDiscovery) addConfiguredExtraFilesystems(extraFilesystems...
method addPartitionExtraFs (line 242) | func (d *diskDiscovery) addPartitionExtraFs(p disk.PartitionStat) {
method addExtraFilesystemFolders (line 253) | func (d *diskDiscovery) addExtraFilesystemFolders(folderNames []string) {
function parseFilesystemEntry (line 38) | func parseFilesystemEntry(entry string) (device, customName string) {
function extraFilesystemPartitionInfo (line 52) | func extraFilesystemPartitionInfo(p disk.PartitionStat) (device, customN...
function isDockerSpecialMountpoint (line 61) | func isDockerSpecialMountpoint(mountpoint string) bool {
function registerFilesystemStats (line 71) | func registerFilesystemStats(existing map[string]*system.FsStats, device...
function isRootFallbackPartition (line 167) | func isRootFallbackPartition(p disk.PartitionStat, rootMountPoint string...
function findPartitionByFilesystemSetting (line 205) | func findPartitionByFilesystemSetting(filesystem string, partitions []di...
method initializeDiskInfo (line 271) | func (a *Agent) initializeDiskInfo() {
method pruneDuplicateRootExtraFilesystems (line 347) | func (a *Agent) pruneDuplicateRootExtraFilesystems() {
function hasSameDiskUsage (line 378) | func hasSameDiskUsage(a, b *disk.UsageStat) bool {
function withinUsageTolerance (line 389) | func withinUsageTolerance(a, b, tolerance uint64) bool {
type ioMatchCandidate (line 396) | type ioMatchCandidate struct
function findIoDevice (line 404) | func findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCo...
function mostActiveIoDevice (line 445) | func mostActiveIoDevice(diskIoCounters map[string]disk.IOCountersStat) s...
function prefixRelated (line 463) | func prefixRelated(a, b string) bool {
function filesystemMatchesPartitionSetting (line 473) | func filesystemMatchesPartitionSetting(filesystem string, p disk.Partiti...
function normalizeDeviceName (line 494) | func normalizeDeviceName(value string) string {
method initializeDiskIoStats (line 503) | func (a *Agent) initializeDiskIoStats(diskIoCounters map[string]disk.IOC...
method updateDiskUsage (line 523) | func (a *Agent) updateDiskUsage(systemStats *system.Stats) {
method updateDiskIo (line 562) | func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Sta...
method getRootMountPoint (line 633) | func (a *Agent) getRootMountPoint() string {
FILE: agent/disk_test.go
function TestParseFilesystemEntry (line 16) | func TestParseFilesystemEntry(t *testing.T) {
function TestExtraFilesystemPartitionInfo (line 96) | func TestExtraFilesystemPartitionInfo(t *testing.T) {
function TestBuildFsStatRegistration (line 137) | func TestBuildFsStatRegistration(t *testing.T) {
function TestAddConfiguredRootFs (line 282) | func TestAddConfiguredRootFs(t *testing.T) {
function TestAddPartitionRootFs (line 349) | func TestAddPartitionRootFs(t *testing.T) {
function TestAddLastResortRootFs (line 382) | func TestAddLastResortRootFs(t *testing.T) {
function TestAddConfiguredExtraFsEntry (line 412) | func TestAddConfiguredExtraFsEntry(t *testing.T) {
function TestAddConfiguredExtraFilesystems (line 477) | func TestAddConfiguredExtraFilesystems(t *testing.T) {
function TestAddExtraFilesystemFolders (line 507) | func TestAddExtraFilesystemFolders(t *testing.T) {
function TestFindIoDevice (line 533) | func TestFindIoDevice(t *testing.T) {
function TestFilesystemMatchesPartitionSetting (line 622) | func TestFilesystemMatchesPartitionSetting(t *testing.T) {
function TestMostActiveIoDevice (line 646) | func TestMostActiveIoDevice(t *testing.T) {
function TestIsDockerSpecialMountpoint (line 668) | func TestIsDockerSpecialMountpoint(t *testing.T) {
function TestInitializeDiskInfoWithCustomNames (line 689) | func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
function TestFsStatsWithCustomNames (line 775) | func TestFsStatsWithCustomNames(t *testing.T) {
function TestExtraFsKeyGeneration (line 790) | func TestExtraFsKeyGeneration(t *testing.T) {
function TestDiskUsageCaching (line 830) | func TestDiskUsageCaching(t *testing.T) {
function TestHasSameDiskUsage (line 913) | func TestHasSameDiskUsage(t *testing.T) {
function TestInitializeDiskIoStatsResetsTrackedDevices (line 947) | func TestInitializeDiskIoStatsResetsTrackedDevices(t *testing.T) {
FILE: agent/docker.go
constant dockerTimeoutMs (line 39) | dockerTimeoutMs = 2100
constant maxNetworkSpeedBps (line 41) | maxNetworkSpeedBps uint64 = 5e9
constant maxMemoryUsage (line 43) | maxMemoryUsage uint64 = 100 * 1024 * 1024 * 1024 * 1024
constant dockerLogsTail (line 45) | dockerLogsTail = 200
constant maxLogFrameSize (line 48) | maxLogFrameSize = 1024 * 1024
constant maxTotalLogSize (line 51) | maxTotalLogSize = 5 * 1024 * 1024
type dockerManager (line 54) | type dockerManager struct
method queue (line 96) | func (d *dockerManager) queue() {
method dequeue (line 104) | func (d *dockerManager) dequeue() {
method shouldExcludeContainer (line 112) | func (dm *dockerManager) shouldExcludeContainer(name string) bool {
method getDockerStats (line 125) | func (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*contai...
method initializeCpuTracking (line 214) | func (dm *dockerManager) initializeCpuTracking(cacheTimeMs uint16) {
method getCpuPreviousValues (line 232) | func (dm *dockerManager) getCpuPreviousValues(cacheTimeMs uint16, cont...
method setCpuCurrentValues (line 237) | func (dm *dockerManager) setCpuCurrentValues(cacheTimeMs uint16, conta...
method getNetworkTracker (line 262) | func (dm *dockerManager) getNetworkTracker(cacheTimeMs uint16, isSent ...
method cycleNetworkDeltasForCacheTime (line 278) | func (dm *dockerManager) cycleNetworkDeltasForCacheTime(cacheTimeMs ui...
method calculateNetworkStats (line 288) | func (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo,...
method getPodmanContainerHealth (line 428) | func (dm *dockerManager) getPodmanContainerHealth(containerID string) ...
method updateContainerStats (line 458) | func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, ...
method deleteContainerStatsSync (line 564) | func (dm *dockerManager) deleteContainerStatsSync(id string) {
method checkDockerVersion (line 683) | func (dm *dockerManager) checkDockerVersion() {
method decode (line 718) | func (dm *dockerManager) decode(resp *http.Response, d any) error {
method getContainerInfo (line 768) | func (dm *dockerManager) getContainerInfo(ctx context.Context, contain...
method getLogs (line 802) | func (dm *dockerManager) getLogs(ctx context.Context, containerID stri...
method GetHostInfo (line 915) | func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err e...
method IsPodman (line 929) | func (dm *dockerManager) IsPodman() bool {
type userAgentRoundTripper (line 84) | type userAgentRoundTripper struct
method RoundTrip (line 90) | func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Re...
function calculateMemoryUsage (line 243) | func calculateMemoryUsage(apiStats *container.ApiStats, isWindows bool) ...
function validateCpuPercentage (line 333) | func validateCpuPercentage(cpuPct float64, containerName string) error {
function updateContainerStatsValues (line 341) | func updateContainerStatsValues(stats *container.Stats, cpuPct float64, ...
function convertContainerPortsToString (line 353) | func convertContainerPortsToString(ctr *container.ApiInfo) string {
function parseDockerStatus (line 384) | func parseDockerStatus(status string) (string, container.DockerHealth) {
function parseDockerHealthStatus (line 420) | func parseDockerHealthStatus(status string) (container.DockerHealth, boo...
function newDockerManager (line 580) | func newDockerManager() *dockerManager {
function getDockerHost (line 734) | func getDockerHost() string {
function validateContainerID (line 745) | func validateContainerID(containerID string) error {
function buildDockerContainerEndpoint (line 752) | func buildDockerContainerEndpoint(containerID, action string, query url....
function detectDockerMultiplexedStream (line 851) | func detectDockerMultiplexedStream(reader *bufio.Reader) bool {
function decodeDockerLogStream (line 868) | func decodeDockerLogStream(reader io.Reader, builder *strings.Builder, m...
FILE: agent/docker_test.go
type recordingRoundTripper (line 29) | type recordingRoundTripper struct
method RoundTrip (line 44) | func (rt *recordingRoundTripper) RoundTrip(req *http.Request) (*http.R...
type roundTripFunc (line 38) | type roundTripFunc
method RoundTrip (line 40) | func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, ...
method cycleCpuDeltas (line 67) | func (dm *dockerManager) cycleCpuDeltas(cacheTimeMs uint16) {
function TestCalculateMemoryUsage (line 77) | func TestCalculateMemoryUsage(t *testing.T) {
function TestBuildDockerContainerEndpoint (line 157) | func TestBuildDockerContainerEndpoint(t *testing.T) {
function TestContainerDetailsRequestsValidateContainerID (line 171) | func TestContainerDetailsRequestsValidateContainerID(t *testing.T) {
function TestContainerDetailsRequestsUseExpectedDockerPaths (line 186) | func TestContainerDetailsRequestsUseExpectedDockerPaths(t *testing.T) {
function TestGetPodmanContainerHealth (line 223) | func TestGetPodmanContainerHealth(t *testing.T) {
function TestValidateCpuPercentage (line 245) | func TestValidateCpuPercentage(t *testing.T) {
function TestUpdateContainerStatsValues (line 300) | func TestUpdateContainerStatsValues(t *testing.T) {
function TestInitializeCpuTracking (line 330) | func TestInitializeCpuTracking(t *testing.T) {
function TestGetCpuPreviousValues (line 360) | func TestGetCpuPreviousValues(t *testing.T) {
function TestSetCpuCurrentValues (line 386) | func TestSetCpuCurrentValues(t *testing.T) {
function TestCalculateNetworkStats (line 406) | func TestCalculateNetworkStats(t *testing.T) {
function TestDockerManagerCreation (line 455) | func TestDockerManagerCreation(t *testing.T) {
function TestCheckDockerVersion (line 472) | func TestCheckDockerVersion(t *testing.T) {
function TestCycleCpuDeltas (line 583) | func TestCycleCpuDeltas(t *testing.T) {
function TestCycleNetworkDeltas (line 612) | func TestCycleNetworkDeltas(t *testing.T) {
function TestConstants (line 639) | func TestConstants(t *testing.T) {
function TestDockerStatsWithMockData (line 646) | func TestDockerStatsWithMockData(t *testing.T) {
function TestMemoryStatsEdgeCases (line 671) | func TestMemoryStatsEdgeCases(t *testing.T) {
function TestContainerStatsInitialization (line 715) | func TestContainerStatsInitialization(t *testing.T) {
function TestCalculateMemoryUsageWithRealData (line 740) | func TestCalculateMemoryUsageWithRealData(t *testing.T) {
function TestCpuPercentageCalculationWithRealData (line 758) | func TestCpuPercentageCalculationWithRealData(t *testing.T) {
function TestNetworkStatsCalculationWithRealData (line 781) | func TestNetworkStatsCalculationWithRealData(t *testing.T) {
function TestContainerStatsEndToEndWithRealData (line 844) | func TestContainerStatsEndToEndWithRealData(t *testing.T) {
function TestGetLogsDetectsMultiplexedWithoutContentType (line 903) | func TestGetLogsDetectsMultiplexedWithoutContentType(t *testing.T) {
function TestGetLogsDoesNotMisclassifyRawStreamAsMultiplexed (line 923) | func TestGetLogsDoesNotMisclassifyRawStreamAsMultiplexed(t *testing.T) {
function TestEdgeCasesWithRealData (line 939) | func TestEdgeCasesWithRealData(t *testing.T) {
function TestDockerStatsWorkflow (line 974) | func TestDockerStatsWorkflow(t *testing.T) {
function TestNetworkRateCalculationFormula (line 1022) | func TestNetworkRateCalculationFormula(t *testing.T) {
function TestGetHostInfo (line 1047) | func TestGetHostInfo(t *testing.T) {
function TestDeltaTrackerCacheTimeIsolation (line 1065) | func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
function TestParseDockerStatus (line 1123) | func TestParseDockerStatus(t *testing.T) {
function TestParseDockerHealthStatus (line 1183) | func TestParseDockerHealthStatus(t *testing.T) {
function TestUpdateContainerStatsUsesPodmanInspectHealthFallback (line 1206) | func TestUpdateContainerStatsUsesPodmanInspectHealthFallback(t *testing....
function TestConstantsAndUtilityFunctions (line 1261) | func TestConstantsAndUtilityFunctions(t *testing.T) {
function TestDecodeDockerLogStream (line 1279) | func TestDecodeDockerLogStream(t *testing.T) {
function TestDecodeDockerLogStreamMemoryProtection (line 1356) | func TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {
function TestShouldExcludeContainer (line 1417) | func TestShouldExcludeContainer(t *testing.T) {
function TestAnsiEscapePattern (line 1521) | func TestAnsiEscapePattern(t *testing.T) {
function TestConvertContainerPortsToString (line 1577) | func TestConvertContainerPortsToString(t *testing.T) {
FILE: agent/emmc_common.go
function isEmmcBlockName (line 9) | func isEmmcBlockName(name string) bool {
function parseHexOrDecByte (line 25) | func parseHexOrDecByte(s string) (uint8, bool) {
function parseHexBytePair (line 42) | func parseHexBytePair(s string) (uint8, uint8, bool) {
function emmcSmartStatus (line 55) | func emmcSmartStatus(preEOL uint8) string {
function emmcPreEOLString (line 68) | func emmcPreEOLString(preEOL uint8) string {
function emmcLifeTimeString (line 81) | func emmcLifeTimeString(v uint8) string {
FILE: agent/emmc_common_test.go
function TestParseHexOrDecByte (line 5) | func TestParseHexOrDecByte(t *testing.T) {
function TestParseHexBytePair (line 28) | func TestParseHexBytePair(t *testing.T) {
function TestEmmcSmartStatus (line 45) | func TestEmmcSmartStatus(t *testing.T) {
function TestIsEmmcBlockName (line 60) | func TestIsEmmcBlockName(t *testing.T) {
FILE: agent/emmc_linux.go
type emmcHealth (line 18) | type emmcHealth struct
function scanEmmcDevices (line 28) | func scanEmmcDevices() []*DeviceInfo {
method collectEmmcHealth (line 59) | func (sm *SmartManager) collectEmmcHealth(deviceInfo *DeviceInfo) (bool,...
function readEmmcHealth (line 123) | func readEmmcHealth(blockName string) (emmcHealth, bool) {
function readLifeTime (line 156) | func readLifeTime(deviceDir string) (uint8, uint8, bool) {
function readBlockCapacityBytes (line 170) | func readBlockCapacityBytes(blockName string) (uint64, bool) {
function readHexByteFile (line 194) | func readHexByteFile(path string) (uint8, bool) {
function hasEmmcHealthFiles (line 203) | func hasEmmcHealthFiles(deviceDir string) bool {
FILE: agent/emmc_linux_test.go
function TestEmmcMockSysfsScanAndCollect (line 13) | func TestEmmcMockSysfsScanAndCollect(t *testing.T) {
FILE: agent/emmc_stub.go
function scanEmmcDevices (line 7) | func scanEmmcDevices() []*DeviceInfo {
method collectEmmcHealth (line 11) | func (sm *SmartManager) collectEmmcHealth(deviceInfo *DeviceInfo) (bool,...
FILE: agent/fingerprint.go
constant fingerprintFileName (line 15) | fingerprintFileName = "fingerprint"
constant knownBadUUID (line 18) | knownBadUUID = "03000200-0400-0500-0006-000700080009"
function GetFingerprint (line 27) | func GetFingerprint(dataDir, hostname, cpuModel string) string {
function generateFingerprint (line 43) | func generateFingerprint(hostname, cpuModel string) string {
function readFingerprint (line 62) | func readFingerprint(dataDir string) (string, error) {
function SaveFingerprint (line 75) | func SaveFingerprint(dataDir, fingerprint string) error {
function DeleteFingerprint (line 81) | func DeleteFingerprint(dataDir string) error {
FILE: agent/fingerprint_test.go
function TestGetFingerprint (line 14) | func TestGetFingerprint(t *testing.T) {
function TestSaveFingerprint (line 60) | func TestSaveFingerprint(t *testing.T) {
function TestDeleteFingerprint (line 82) | func TestDeleteFingerprint(t *testing.T) {
FILE: agent/gpu.go
constant nvidiaSmiCmd (line 24) | nvidiaSmiCmd string = "nvidia-smi"
constant rocmSmiCmd (line 25) | rocmSmiCmd string = "rocm-smi"
constant tegraStatsCmd (line 26) | tegraStatsCmd string = "tegrastats"
constant nvtopCmd (line 27) | nvtopCmd string = "nvtop"
constant powermetricsCmd (line 28) | powermetricsCmd string = "powermetrics"
constant macmonCmd (line 29) | macmonCmd string = "macmon"
constant noGPUFoundMsg (line 30) | noGPUFoundMsg string = "no GPU found - see https://beszel.dev/guide/gpu"
constant retryWaitTime (line 33) | retryWaitTime time.Duration = 5 * time.Second
constant maxFailureRetries (line 34) | maxFailureRetries int = 5
constant mebibytesInAMegabyte (line 37) | mebibytesInAMegabyte float64 = 1.024
constant milliwattsInAWatt (line 38) | milliwattsInAWatt float64 = 1000.0
type GPUManager (line 42) | type GPUManager struct
method getJetsonParser (line 184) | func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
method parseNvidiaData (line 235) | func (gm *GPUManager) parseNvidiaData(output []byte) bool {
method parseAmdData (line 271) | func (gm *GPUManager) parseAmdData(output []byte) bool {
method GetCurrentData (line 305) | func (gm *GPUManager) GetCurrentData(cacheKey uint16) map[string]syste...
method initializeSnapshots (line 329) | func (gm *GPUManager) initializeSnapshots(cacheKey uint16) {
method countGPUNames (line 342) | func (gm *GPUManager) countGPUNames() map[string]int {
method calculateGPUAverage (line 351) | func (gm *GPUManager) calculateGPUAverage(id string, gpu *system.GPUDa...
method calculateDeltaCount (line 387) | func (gm *GPUManager) calculateDeltaCount(currentCount uint32, lastSna...
method calculateDeltas (line 395) | func (gm *GPUManager) calculateDeltas(gpu *system.GPUData, lastSnapsho...
method calculateIntelGPUUsage (line 405) | func (gm *GPUManager) calculateIntelGPUUsage(gpuAvg, gpu *system.GPUDa...
method updateInstantaneousValues (line 421) | func (gm *GPUManager) updateInstantaneousValues(gpuAvg *system.GPUData...
method storeSnapshot (line 428) | func (gm *GPUManager) storeSnapshot(id string, gpu *system.GPUData, ca...
method discoverGpuCapabilities (line 444) | func (gm *GPUManager) discoverGpuCapabilities() gpuCapabilities {
method startIntelCollector (line 478) | func (gm *GPUManager) startIntelCollector() {
method startNvidiaSmiCollector (line 495) | func (gm *GPUManager) startNvidiaSmiCollector(intervalSeconds string) {
method startTegraStatsCollector (line 509) | func (gm *GPUManager) startTegraStatsCollector(intervalMilliseconds st...
method startRocmSmiCollector (line 519) | func (gm *GPUManager) startRocmSmiCollector(pollInterval time.Duration) {
method collectorDefinitions (line 541) | func (gm *GPUManager) collectorDefinitions(caps gpuCapabilities) map[c...
method startNvmlCollector (line 626) | func (gm *GPUManager) startNvmlCollector() bool {
method startAmdSysfsCollector (line 637) | func (gm *GPUManager) startAmdSysfsCollector() bool {
method startCollectorsByPriority (line 647) | func (gm *GPUManager) startCollectorsByPriority(priorities []collector...
method resolveLegacyCollectorPriority (line 687) | func (gm *GPUManager) resolveLegacyCollectorPriority(caps gpuCapabilit...
type gpuSnapshot (line 54) | type gpuSnapshot struct
type RocmSmiJson (line 63) | type RocmSmiJson struct
type gpuCollector (line 75) | type gpuCollector struct
method start (line 138) | func (c *gpuCollector) start() {
method collect (line 154) | func (c *gpuCollector) collect() error {
type collectorSource (line 86) | type collectorSource
constant collectorSourceNVTop (line 89) | collectorSourceNVTop collectorSource = collectorSource(nvtopCmd)
constant collectorSourceNVML (line 90) | collectorSourceNVML collectorSource = "nvml"
constant collectorSourceNvidiaSMI (line 91) | collectorSourceNvidiaSMI collectorSource = collectorSource(nvidiaSmiCmd)
constant collectorSourceIntelGpuTop (line 92) | collectorSourceIntelGpuTop collectorSource = collectorSource(intelGpuSt...
constant collectorSourceAmdSysfs (line 93) | collectorSourceAmdSysfs collectorSource = "amd_sysfs"
constant collectorSourceRocmSMI (line 94) | collectorSourceRocmSMI collectorSource = collectorSource(rocmSmiCmd)
constant collectorSourceMacmon (line 95) | collectorSourceMacmon collectorSource = collectorSource(macmonCmd)
constant collectorSourcePowermetrics (line 96) | collectorSourcePowermetrics collectorSource = collectorSource(powermetri...
constant collectorGroupNvidia (line 97) | collectorGroupNvidia string = "nvidia"
constant collectorGroupIntel (line 98) | collectorGroupIntel string = "intel"
constant collectorGroupAmd (line 99) | collectorGroupAmd string = "amd"
constant collectorGroupApple (line 100) | collectorGroupApple string = "apple"
function isValidCollectorSource (line 103) | func isValidCollectorSource(source collectorSource) bool {
type gpuCapabilities (line 119) | type gpuCapabilities struct
type collectorDefinition (line 130) | type collectorDefinition struct
function hasAnyGpuCollector (line 474) | func hasAnyGpuCollector(caps gpuCapabilities) bool {
function parseCollectorPriority (line 609) | func parseCollectorPriority(value string) []collectorSource {
function NewGPUManager (line 731) | func NewGPUManager() (*GPUManager, error) {
FILE: agent/gpu_amd_linux.go
method hasAmdSysfs (line 30) | func (gm *GPUManager) hasAmdSysfs() bool {
method collectAmdStats (line 45) | func (gm *GPUManager) collectAmdStats() error {
function isAmdGpu (line 90) | func isAmdGpu(cardPath string) bool {
method updateAmdGpuData (line 100) | func (gm *GPUManager) updateAmdGpuData(cardPath string) bool {
function readSysfsFloat (line 156) | func readSysfsFloat(path string) (float64, error) {
function normalizeHexID (line 165) | func normalizeHexID(id string) string {
function cacheKeyForAmdgpu (line 170) | func cacheKeyForAmdgpu(deviceID, revisionID string) string {
function lookupAmdgpuNameInFile (line 178) | func lookupAmdgpuNameInFile(deviceID, revisionID, filePath string) (name...
function getCachedAmdgpuName (line 217) | func getCachedAmdgpuName(deviceID, revisionID string) (name string, foun...
function normalizeAmdgpuName (line 243) | func normalizeAmdgpuName(name string) string {
function cacheAmdgpuName (line 251) | func cacheAmdgpuName(deviceID, revisionID, name string, exact bool) {
function cacheMissingAmdgpuName (line 262) | func cacheMissingAmdgpuName(deviceID, revisionID string) {
function getAmdGpuName (line 274) | func getAmdGpuName(devicePath string) string {
FILE: agent/gpu_amd_linux_test.go
function TestNormalizeHexID (line 16) | func TestNormalizeHexID(t *testing.T) {
function TestCacheKeyForAmdgpu (line 39) | func TestCacheKeyForAmdgpu(t *testing.T) {
function TestReadSysfsFloat (line 55) | func TestReadSysfsFloat(t *testing.T) {
function TestIsAmdGpu (line 82) | func TestIsAmdGpu(t *testing.T) {
function TestAmdgpuNameCacheRoundTrip (line 100) | func TestAmdgpuNameCacheRoundTrip(t *testing.T) {
function TestUpdateAmdGpuDataWithFakeSysfs (line 122) | func TestUpdateAmdGpuDataWithFakeSysfs(t *testing.T) {
function TestLookupAmdgpuNameInFile (line 187) | func TestLookupAmdgpuNameInFile(t *testing.T) {
function TestGetAmdGpuNameFromIdsFile (line 249) | func TestGetAmdGpuNameFromIdsFile(t *testing.T) {
FILE: agent/gpu_amd_unsupported.go
method hasAmdSysfs (line 9) | func (gm *GPUManager) hasAmdSysfs() bool {
method collectAmdStats (line 13) | func (gm *GPUManager) collectAmdStats() error {
FILE: agent/gpu_darwin.go
constant powermetricsSampleIntervalMs (line 21) | powermetricsSampleIntervalMs = 500
constant powermetricsPollInterval (line 23) | powermetricsPollInterval = 2 * time.Second
constant macmonIntervalMs (line 25) | macmonIntervalMs = 2500
constant appleGPUID (line 28) | appleGPUID = "0"
method startPowermetricsCollector (line 32) | func (gm *GPUManager) startPowermetricsCollector() {
method collectPowermetrics (line 58) | func (gm *GPUManager) collectPowermetrics() error {
method parsePowermetricsData (line 80) | func (gm *GPUManager) parsePowermetricsData(output []byte) bool {
method startMacmonCollector (line 137) | func (gm *GPUManager) startMacmonCollector() {
type macmonTemp (line 162) | type macmonTemp struct
type macmonSample (line 166) | type macmonSample struct
method collectMacmonPipe (line 173) | func (gm *GPUManager) collectMacmonPipe() (err error) {
method parseMacmonLine (line 218) | func (gm *GPUManager) parseMacmonLine(line []byte) bool {
FILE: agent/gpu_darwin_test.go
function TestParsePowermetricsData (line 13) | func TestParsePowermetricsData(t *testing.T) {
function TestParsePowermetricsDataPartial (line 44) | func TestParsePowermetricsDataPartial(t *testing.T) {
function TestParseMacmonLine (line 63) | func TestParseMacmonLine(t *testing.T) {
FILE: agent/gpu_darwin_unsupported.go
method startPowermetricsCollector (line 6) | func (gm *GPUManager) startPowermetricsCollector() {}
method startMacmonCollector (line 9) | func (gm *GPUManager) startMacmonCollector() {}
FILE: agent/gpu_intel.go
constant intelGpuStatsCmd (line 15) | intelGpuStatsCmd string = "intel_gpu_top"
constant intelGpuStatsInterval (line 16) | intelGpuStatsInterval string = "3300"
type intelGpuStats (line 19) | type intelGpuStats struct
method updateIntelFromStats (line 26) | func (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {
method collectIntelStats (line 53) | func (gm *GPUManager) collectIntelStats() (err error) {
method parseIntelHeaders (line 132) | func (gm *GPUManager) parseIntelHeaders(header1 string, header2 string) ...
method parseIntelData (line 174) | func (gm *GPUManager) parseIntelData(line string, engineNames []string, ...
FILE: agent/gpu_nvml.go
constant nvmlSuccess (line 18) | nvmlSuccess int = 0
type nvmlDevice (line 21) | type nvmlDevice
type nvmlReturn (line 23) | type nvmlReturn
type nvmlMemoryV1 (line 25) | type nvmlMemoryV1 struct
type nvmlMemoryV2 (line 31) | type nvmlMemoryV2 struct
type nvmlUtilization (line 39) | type nvmlUtilization struct
type nvmlPciInfo (line 44) | type nvmlPciInfo struct
type nvmlCollector (line 68) | type nvmlCollector struct
method init (line 76) | func (c *nvmlCollector) init() error {
method start (line 134) | func (c *nvmlCollector) start() {
method collect (line 143) | func (c *nvmlCollector) collect() {
FILE: agent/gpu_nvml_linux.go
function openLibrary (line 14) | func openLibrary(name string) (uintptr, error) {
function getNVMLPath (line 18) | func getNVMLPath() string {
function hasSymbol (line 22) | func hasSymbol(lib uintptr, symbol string) bool {
method isGPUActive (line 27) | func (c *nvmlCollector) isGPUActive(bdf string) bool {
FILE: agent/gpu_nvml_unsupported.go
type nvmlCollector (line 7) | type nvmlCollector struct
method init (line 11) | func (c *nvmlCollector) init() error {
method start (line 15) | func (c *nvmlCollector) start() {}
FILE: agent/gpu_nvml_windows.go
function openLibrary (line 9) | func openLibrary(name string) (uintptr, error) {
function getNVMLPath (line 14) | func getNVMLPath() string {
function hasSymbol (line 18) | func hasSymbol(lib uintptr, symbol string) bool {
method isGPUActive (line 23) | func (c *nvmlCollector) isGPUActive(bdf string) bool {
FILE: agent/gpu_nvtop.go
type nvtopSnapshot (line 16) | type nvtopSnapshot struct
function parseNvtopNumber (line 26) | func parseNvtopNumber(raw string) float64 {
method parseNvtopData (line 36) | func (gm *GPUManager) parseNvtopData(output []byte) bool {
method updateNvtopSnapshots (line 45) | func (gm *GPUManager) updateNvtopSnapshots(snapshots []nvtopSnapshot) bo...
method collectNvtopStats (line 103) | func (gm *GPUManager) collectNvtopStats(interval string) error {
method startNvtopCollector (line 140) | func (gm *GPUManager) startNvtopCollector(interval string, onFailure fun...
FILE: agent/gpu_test.go
function TestParseNvidiaData (line 20) | func TestParseNvidiaData(t *testing.T) {
function TestParseAmdData (line 137) | func TestParseAmdData(t *testing.T) {
function TestParseNvtopData (line 253) | func TestParseNvtopData(t *testing.T) {
function TestUpdateNvtopSnapshotsKeepsDeviceAssociationWhenOrderChanges (line 284) | func TestUpdateNvtopSnapshotsKeepsDeviceAssociationWhenOrderChanges(t *t...
function TestParseCollectorPriority (line 334) | func TestParseCollectorPriority(t *testing.T) {
function TestParseJetsonData (line 347) | func TestParseJetsonData(t *testing.T) {
function TestGetCurrentData (line 443) | func TestGetCurrentData(t *testing.T) {
function TestCalculateDeltaCount (line 629) | func TestCalculateDeltaCount(t *testing.T) {
function TestCalculateDeltas (line 650) | func TestCalculateDeltas(t *testing.T) {
function TestCalculateIntelGPUUsage (line 683) | func TestCalculateIntelGPUUsage(t *testing.T) {
function TestUpdateInstantaneousValues (line 756) | func TestUpdateInstantaneousValues(t *testing.T) {
function TestStoreSnapshot (line 779) | func TestStoreSnapshot(t *testing.T) {
function TestCountGPUNames (line 851) | func TestCountGPUNames(t *testing.T) {
function TestInitializeSnapshots (line 891) | func TestInitializeSnapshots(t *testing.T) {
function TestCalculateGPUAverage (line 934) | func TestCalculateGPUAverage(t *testing.T) {
function TestGPUCapabilitiesAndLegacyPriority (line 1084) | func TestGPUCapabilitiesAndLegacyPriority(t *testing.T) {
function TestCollectorStartHelpers (line 1236) | func TestCollectorStartHelpers(t *testing.T) {
function TestNewGPUManagerPriorityNvtopFallback (line 1372) | func TestNewGPUManagerPriorityNvtopFallback(t *testing.T) {
function TestNewGPUManagerPriorityMixedCollectors (line 1401) | func TestNewGPUManagerPriorityMixedCollectors(t *testing.T) {
function TestNewGPUManagerPriorityNvmlFallbackToNvidiaSmi (line 1435) | func TestNewGPUManagerPriorityNvmlFallbackToNvidiaSmi(t *testing.T) {
function TestNewGPUManagerConfiguredCollectorsMustStart (line 1458) | func TestNewGPUManagerConfiguredCollectorsMustStart(t *testing.T) {
function TestNewGPUManagerJetsonIgnoresCollectorConfig (line 1482) | func TestNewGPUManagerJetsonIgnoresCollectorConfig(t *testing.T) {
function TestAccumulation (line 1506) | func TestAccumulation(t *testing.T) {
function TestIntelUpdateFromStats (line 1673) | func TestIntelUpdateFromStats(t *testing.T) {
function TestIntelCollectorStreaming (line 1721) | func TestIntelCollectorStreaming(t *testing.T) {
function TestParseIntelHeaders (line 1764) | func TestParseIntelHeaders(t *testing.T) {
function TestParseIntelData (line 1852) | func TestParseIntelData(t *testing.T) {
function TestIntelCollectorDeviceEnv (line 1980) | func TestIntelCollectorDeviceEnv(t *testing.T) {
FILE: agent/handlers.go
type HandlerContext (line 16) | type HandlerContext struct
type RequestHandler (line 27) | type RequestHandler interface
type Responder (line 33) | type Responder interface
type HandlerRegistry (line 38) | type HandlerRegistry struct
method Register (line 59) | func (hr *HandlerRegistry) Register(action common.WebSocketAction, han...
method Handle (line 64) | func (hr *HandlerRegistry) Handle(hctx *HandlerContext) error {
method GetHandler (line 82) | func (hr *HandlerRegistry) GetHandler(action common.WebSocketAction) (...
function NewHandlerRegistry (line 43) | func NewHandlerRegistry() *HandlerRegistry {
type GetDataHandler (line 91) | type GetDataHandler struct
method Handle (line 93) | func (h *GetDataHandler) Handle(hctx *HandlerContext) error {
type CheckFingerprintHandler (line 105) | type CheckFingerprintHandler struct
method Handle (line 107) | func (h *CheckFingerprintHandler) Handle(hctx *HandlerContext) error {
type GetContainerLogsHandler (line 115) | type GetContainerLogsHandler struct
method Handle (line 117) | func (h *GetContainerLogsHandler) Handle(hctx *HandlerContext) error {
type GetContainerInfoHandler (line 140) | type GetContainerInfoHandler struct
method Handle (line 142) | func (h *GetContainerInfoHandler) Handle(hctx *HandlerContext) error {
type GetSmartDataHandler (line 165) | type GetSmartDataHandler struct
method Handle (line 167) | func (h *GetSmartDataHandler) Handle(hctx *HandlerContext) error {
type GetSystemdInfoHandler (line 184) | type GetSystemdInfoHandler struct
method Handle (line 186) | func (h *GetSystemdInfoHandler) Handle(hctx *HandlerContext) error {
FILE: agent/handlers_test.go
type MockHandler (line 14) | type MockHandler struct
method Handle (line 20) | func (m *MockHandler) Handle(ctx *HandlerContext) error {
method RequiresVerification (line 27) | func (m *MockHandler) RequiresVerification() bool {
function TestHandlerRegistry (line 32) | func TestHandlerRegistry(t *testing.T) {
function TestCheckFingerprintHandler (line 93) | func TestCheckFingerprintHandler(t *testing.T) {
FILE: agent/health/health.go
function getHealthFilePath (line 19) | func getHealthFilePath() string {
function updateHealthFile (line 30) | func updateHealthFile(path string) error {
function Check (line 39) | func Check() error {
function Update (line 52) | func Update() error {
function CleanUp (line 57) | func CleanUp() error {
FILE: agent/health/health_test.go
function TestHealth (line 17) | func TestHealth(t *testing.T) {
FILE: agent/lhm/beszel_lhm.cs
class Program (line 5) | class Program
method Main (line 7) | static void Main()
method ProcessSensors (line 49) | static void ProcessSensors(IHardware hardware, System.IO.TextWriter wr...
FILE: agent/mdraid_linux.go
type mdraidHealth (line 19) | type mdraidHealth struct
function scanMdraidDevices (line 32) | func scanMdraidDevices() []*DeviceInfo {
method collectMdraidHealth (line 63) | func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (boo...
function readMdraidHealth (line 130) | func readMdraidHealth(blockName string) (mdraidHealth, bool) {
function mdraidSmartStatus (line 167) | func mdraidSmartStatus(health mdraidHealth) string {
function isMdraidBlockName (line 195) | func isMdraidBlockName(name string) bool {
function readMdraidBlockCapacityBytes (line 212) | func readMdraidBlockCapacityBytes(blockName, root string) (uint64, bool) {
FILE: agent/mdraid_linux_test.go
function TestMdraidMockSysfsScanAndCollect (line 13) | func TestMdraidMockSysfsScanAndCollect(t *testing.T) {
function TestMdraidSmartStatus (line 84) | func TestMdraidSmartStatus(t *testing.T) {
FILE: agent/mdraid_stub.go
function scanMdraidDevices (line 5) | func scanMdraidDevices() []*DeviceInfo {
method collectMdraidHealth (line 9) | func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (boo...
FILE: agent/network.go
type NicConfig (line 23) | type NicConfig struct
function newNicConfig (line 29) | func newNicConfig(nicsEnvVal string) *NicConfig {
function isValidNic (line 50) | func isValidNic(nicName string, cfg *NicConfig) bool {
method updateNetworkStats (line 79) | func (a *Agent) updateNetworkStats(cacheTimeMs uint16, systemStats *syst...
method initializeNetIoStats (line 93) | func (a *Agent) initializeNetIoStats() {
method ensureNetInterfacesInitialized (line 122) | func (a *Agent) ensureNetInterfacesInitialized() {
method ensureNetworkInterfacesMap (line 133) | func (a *Agent) ensureNetworkInterfacesMap(systemStats *system.Stats) {
method loadAndTickNetBaseline (line 140) | func (a *Agent) loadAndTickNetBaseline(cacheTimeMs uint16) (netIoStat sy...
method sumAndTrackPerNicDeltas (line 153) | func (a *Agent) sumAndTrackPerNicDeltas(cacheTimeMs uint16, msElapsed ui...
method computeBytesPerSecond (line 199) | func (a *Agent) computeBytesPerSecond(msElapsed, totalBytesSent, totalBy...
method applyNetworkTotals (line 208) | func (a *Agent) applyNetworkTotals(
function skipNetworkInterface (line 238) | func skipNetworkInterface(v psutilNet.IOCountersStat, nicCfg *NicConfig)...
FILE: agent/network_test.go
function TestIsValidNic (line 16) | func TestIsValidNic(t *testing.T) {
function TestNewNicConfig (line 156) | func TestNewNicConfig(t *testing.T) {
function TestSkipNetworkInterface (line 264) | func TestSkipNetworkInterface(t *testing.T) {
function TestEnsureNetworkInterfacesMap (line 297) | func TestEnsureNetworkInterfacesMap(t *testing.T) {
function TestLoadAndTickNetBaseline (line 311) | func TestLoadAndTickNetBaseline(t *testing.T) {
function TestComputeBytesPerSecond (line 330) | func TestComputeBytesPerSecond(t *testing.T) {
function TestSumAndTrackPerNicDeltas (line 345) | func TestSumAndTrackPerNicDeltas(t *testing.T) {
function TestSumAndTrackPerNicDeltasHandlesCounterReset (line 374) | func TestSumAndTrackPerNicDeltasHandlesCounterReset(t *testing.T) {
function TestApplyNetworkTotals (line 411) | func TestApplyNetworkTotals(t *testing.T) {
FILE: agent/response.go
function newAgentResponse (line 13) | func newAgentResponse(data any, requestID *uint32) common.AgentResponse {
FILE: agent/sensors.go
type SensorConfig (line 20) | type SensorConfig struct
method newSensorConfig (line 29) | func (a *Agent) newSensorConfig() *SensorConfig {
type getTempsFn (line 39) | type getTempsFn
method newSensorConfigWithEnv (line 43) | func (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensor...
method updateTemperatures (line 79) | func (a *Agent) updateTemperatures(systemStats *system.Stats) {
method getTempsWithPanicRecovery (line 144) | func (a *Agent) getTempsWithPanicRecovery(getTemps getTempsFn) (temps []...
function isValidSensor (line 156) | func isValidSensor(sensorName string, config *SensorConfig) bool {
function scaleTemperature (line 186) | func scaleTemperature(temp float64) float64 {
FILE: agent/sensors_test.go
function TestIsValidSensor (line 19) | func TestIsValidSensor(t *testing.T) {
function TestNewSensorConfigWithEnv (line 163) | func TestNewSensorConfigWithEnv(t *testing.T) {
function TestNewSensorConfig (line 331) | func TestNewSensorConfig(t *testing.T) {
function TestScaleTemperature (line 379) | func TestScaleTemperature(t *testing.T) {
function TestScaleTemperatureLogic (line 428) | func TestScaleTemperatureLogic(t *testing.T) {
function TestGetTempsWithPanicRecovery (line 461) | func TestGetTempsWithPanicRecovery(t *testing.T) {
FILE: agent/sensors_windows.go
type lhmProcess (line 31) | type lhmProcess struct
method startProcess (line 83) | func (lhm *lhmProcess) startProcess() error {
method cleanupProcess (line 119) | func (lhm *lhmProcess) cleanupProcess() {
method getTemps (line 142) | func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors....
method cleanup (line 247) | func (lhm *lhmProcess) cleanup() {
function newlhmProcess (line 55) | func newlhmProcess() (*lhmProcess, error) {
function getSensorTemps (line 219) | func getSensorTemps(ctx context.Context) (temps []sensors.TemperatureSta...
function copyEmbeddedDir (line 255) | func copyEmbeddedDir(fs embed.FS, srcPath, destPath string) error {
FILE: agent/server.go
type ServerOptions (line 26) | type ServerOptions struct
method StartServer (line 39) | func (a *Agent) StartServer(opts ServerOptions) error {
method getHubVersion (line 105) | func (a *Agent) getHubVersion(sessionId string, sessionCtx ssh.Context) ...
method handleSession (line 126) | func (a *Agent) handleSession(s ssh.Session) {
method handleSSHRequest (line 163) | func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbo...
method handleLegacyStats (line 195) | func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version...
method writeToSession (line 203) | func (a *Agent) writeToSession(w io.Writer, stats *system.CombinedData, ...
function extractHubVersion (line 212) | func extractHubVersion(versionString string) (semver.Version, error) {
function ParseKeys (line 219) | func ParseKeys(input string) ([]gossh.PublicKey, error) {
function GetAddress (line 240) | func GetAddress(addr string) string {
function GetNetwork (line 261) | func GetNetwork(addr string) string {
method StopServer (line 273) | func (a *Agent) StopServer() error {
FILE: agent/server_test.go
function TestStartServer (line 29) | func TestStartServer(t *testing.T) {
function TestStartServerDisableSSH (line 185) | func TestStartServerDisableSSH(t *testing.T) {
function createTempFile (line 207) | func createTempFile(content string) (string, error) {
function TestParseSingleKeyFromString (line 222) | func TestParseSingleKeyFromString(t *testing.T) {
function TestParseMultipleKeysFromString (line 237) | func TestParseMultipleKeysFromString(t *testing.T) {
function TestParseSingleKeyFromFile (line 252) | func TestParseSingleKeyFromFile(t *testing.T) {
function TestParseMultipleKeysFromFile (line 280) | func TestParseMultipleKeysFromFile(t *testing.T) {
function TestParseInvalidKey (line 308) | func TestParseInvalidKey(t *testing.T) {
function TestExtractHubVersion (line 324) | func TestExtractHubVersion(t *testing.T) {
function TestGetHubVersion (line 398) | func TestGetHubVersion(t *testing.T) {
type mockSSHContext (line 433) | type mockSSHContext struct
method SessionID (line 440) | func (m *mockSSHContext) SessionID() string {
method ClientVersion (line 444) | func (m *mockSSHContext) ClientVersion() string {
method ServerVersion (line 448) | func (m *mockSSHContext) ServerVersion() string {
method Value (line 452) | func (m *mockSSHContext) Value(key interface{}) interface{} {
method User (line 459) | func (m *mockSSHContext) User() string { return "te...
method RemoteAddr (line 460) | func (m *mockSSHContext) RemoteAddr() net.Addr { return nil }
method LocalAddr (line 461) | func (m *mockSSHContext) LocalAddr() net.Addr { return nil }
method Permissions (line 462) | func (m *mockSSHContext) Permissions() *ssh.Permissions { return nil }
method SetValue (line 463) | func (m *mockSSHContext) SetValue(key, value interface{}) {}
function TestWriteToSessionEncoding (line 470) | func TestWriteToSessionEncoding(t *testing.T) {
function createTestCombinedData (line 561) | func createTestCombinedData() *system.CombinedData {
function TestHubVersionCaching (line 589) | func TestHubVersionCaching(t *testing.T) {
FILE: agent/smart.go
type SmartManager (line 26) | type SmartManager struct
method Refresh (line 66) | func (sm *SmartManager) Refresh(forceScan bool) error {
method devicesSnapshot (line 92) | func (sm *SmartManager) devicesSnapshot() []*DeviceInfo {
method resolveRefreshError (line 110) | func (sm *SmartManager) resolveRefreshError(scanErr, collectErr error)...
method GetCurrentData (line 136) | func (sm *SmartManager) GetCurrentData() map[string]smart.SmartData {
method ScanDevices (line 152) | func (sm *SmartManager) ScanDevices(force bool) error {
method parseConfiguredDevices (line 225) | func (sm *SmartManager) parseConfiguredDevices(config string) ([]*Devi...
method refreshExcludedDevices (line 263) | func (sm *SmartManager) refreshExcludedDevices() {
method isExcludedDevice (line 276) | func (sm *SmartManager) isExcludedDevice(deviceName string) bool {
method filterExcludedDevices (line 281) | func (sm *SmartManager) filterExcludedDevices(devices []*DeviceInfo) [...
method parseSmartOutput (line 363) | func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, outpu...
method CollectSmart (line 456) | func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
method smartctlArgs (line 550) | func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeSt...
method hasDataForDevice (line 584) | func (sm *SmartManager) hasDataForDevice(deviceName string) bool {
method parseScan (line 598) | func (sm *SmartManager) parseScan(output []byte) ([]*DeviceInfo, bool) {
method updateSmartDevices (line 756) | func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {
method isVirtualDevice (line 795) | func (sm *SmartManager) isVirtualDevice(data *smart.SmartInfoForSata) ...
method isVirtualDeviceNvme (line 804) | func (sm *SmartManager) isVirtualDeviceNvme(data *smart.SmartInfoForNv...
method isVirtualDeviceScsi (line 811) | func (sm *SmartManager) isVirtualDeviceScsi(data *smart.SmartInfoForSc...
method isVirtualDeviceFromStrings (line 820) | func (sm *SmartManager) isVirtualDeviceFromStrings(fields ...string) b...
method parseSmartForSata (line 838) | func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
method parseSmartForScsi (line 950) | func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
method parseSmartForNvme (line 1038) | func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
method detectSmartctl (line 1106) | func (sm *SmartManager) detectSmartctl() (string, error) {
type scanOutput (line 36) | type scanOutput struct
type DeviceInfo (line 45) | type DeviceInfo struct
type deviceKey (line 58) | type deviceKey struct
function detectSmartOutputType (line 307) | func detectSmartOutputType(output []byte) string {
function hasJSONValue (line 333) | func hasJSONValue(raw json.RawMessage) bool {
function normalizeParserType (line 341) | func normalizeParserType(value string) string {
function makeDeviceKey (line 357) | func makeDeviceKey(name, deviceType string) deviceKey {
function mergeDeviceLists (line 626) | func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*De...
function getSmartStatus (line 910) | func getSmartStatus(temperature uint8, passed bool) string {
function findAtaDeviceStatisticsValue (line 922) | func findAtaDeviceStatisticsValue(data *smart.SmartInfoForSata, ataDevic...
function parseScsiGigabytesProcessed (line 1024) | func parseScsiGigabytesProcessed(value string) int64 {
function isNvmeControllerPath (line 1137) | func isNvmeControllerPath(path string) bool {
function NewSmartManager (line 1157) | func NewSmartManager() (*SmartManager, error) {
FILE: agent/smart_nonwindows.go
function ensureEmbeddedSmartctl (line 7) | func ensureEmbeddedSmartctl() (string, error) {
FILE: agent/smart_test.go
function TestParseSmartForScsi (line 16) | func TestParseSmartForScsi(t *testing.T) {
function TestParseSmartForSata (line 63) | func TestParseSmartForSata(t *testing.T) {
function TestParseSmartForSataDeviceStatisticsTemperature (line 91) | func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
function TestParseSmartForSataAtaDeviceStatistics (line 124) | func TestParseSmartForSataAtaDeviceStatistics(t *testing.T) {
function TestParseSmartForSataNegativeDeviceStatistics (line 159) | func TestParseSmartForSataNegativeDeviceStatistics(t *testing.T) {
function TestParseSmartForSataParentheticalRawValue (line 196) | func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
function TestParseSmartForNvme (line 239) | func TestParseSmartForNvme(t *testing.T) {
function TestHasDataForDevice (line 268) | func TestHasDataForDevice(t *testing.T) {
function TestDevicesSnapshotReturnsCopy (line 280) | func TestDevicesSnapshotReturnsCopy(t *testing.T) {
function TestScanDevicesWithEnvOverrideAndSeparator (line 302) | func TestScanDevicesWithEnvOverrideAndSeparator(t *testing.T) {
function TestScanDevicesWithEnvOverride (line 320) | func TestScanDevicesWithEnvOverride(t *testing.T) {
function TestScanDevicesWithEnvOverrideInvalid (line 337) | func TestScanDevicesWithEnvOverrideInvalid(t *testing.T) {
function TestScanDevicesWithEnvOverrideEmpty (line 348) | func TestScanDevicesWithEnvOverrideEmpty(t *testing.T) {
function TestSmartctlArgsWithoutType (line 360) | func TestSmartctlArgsWithoutType(t *testing.T) {
function TestSmartctlArgs (line 369) | func TestSmartctlArgs(t *testing.T) {
function TestResolveRefreshError (line 395) | func TestResolveRefreshError(t *testing.T) {
function TestParseScan (line 474) | func TestParseScan(t *testing.T) {
function TestMergeDeviceListsPrefersConfigured (line 507) | func TestMergeDeviceListsPrefersConfigured(t *testing.T) {
function TestMergeDeviceListsPreservesVerification (line 537) | func TestMergeDeviceListsPreservesVerification(t *testing.T) {
function TestMergeDeviceListsUpdatesTypeWhenUnverified (line 555) | func TestMergeDeviceListsUpdatesTypeWhenUnverified(t *testing.T) {
function TestMergeDeviceListsHandlesDevicesWithSameNameAndDifferentTypes (line 573) | func TestMergeDeviceListsHandlesDevicesWithSameNameAndDifferentTypes(t *...
function TestMergeDeviceListsHandlesMixedRAIDAndRegular (line 596) | func TestMergeDeviceListsHandlesMixedRAIDAndRegular(t *testing.T) {
function TestUpdateSmartDevicesPreservesRAIDDrives (line 620) | func TestUpdateSmartDevicesPreservesRAIDDrives(t *testing.T) {
function TestParseSmartOutputMarksVerified (line 655) | func TestParseSmartOutputMarksVerified(t *testing.T) {
function TestParseSmartOutputKeepsCustomType (line 669) | func TestParseSmartOutputKeepsCustomType(t *testing.T) {
function TestParseSmartOutputResetsVerificationOnFailure (line 683) | func TestParseSmartOutputResetsVerificationOnFailure(t *testing.T) {
function assertAttrValue (line 692) | func assertAttrValue(t *testing.T, attributes []*smart.SmartAttribute, n...
function findAttr (line 703) | func findAttr(attributes []*smart.SmartAttribute, name string) *smart.Sm...
function TestIsVirtualDevice (line 712) | func TestIsVirtualDevice(t *testing.T) {
function TestIsVirtualDeviceNvme (line 744) | func TestIsVirtualDeviceNvme(t *testing.T) {
function TestIsVirtualDeviceScsi (line 770) | func TestIsVirtualDeviceScsi(t *testing.T) {
function TestFindAtaDeviceStatisticsValue (line 802) | func TestFindAtaDeviceStatisticsValue(t *testing.T) {
function TestRefreshExcludedDevices (line 978) | func TestRefreshExcludedDevices(t *testing.T) {
function TestIsExcludedDevice (line 1049) | func TestIsExcludedDevice(t *testing.T) {
function TestFilterExcludedDevices (line 1077) | func TestFilterExcludedDevices(t *testing.T) {
function TestIsNvmeControllerPath (line 1170) | func TestIsNvmeControllerPath(t *testing.T) {
FILE: agent/smart_windows.go
function ensureEmbeddedSmartctl (line 22) | func ensureEmbeddedSmartctl() (string, error) {
FILE: agent/system.go
type prevDisk (line 27) | type prevDisk struct
method refreshSystemDetails (line 34) | func (a *Agent) refreshSystemDetails() {
method getSystemStats (line 119) | func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
function getOsPrettyName (line 255) | func getOsPrettyName() (string, error) {
FILE: agent/systemd.go
type systemdManager (line 25) | type systemdManager struct
method startWorker (line 79) | func (sm *systemdManager) startWorker(conn *dbus.Conn) {
method getServiceStatsCount (line 96) | func (sm *systemdManager) getServiceStatsCount() int {
method getFailedServiceCount (line 101) | func (sm *systemdManager) getFailedServiceCount() uint16 {
method getServiceStats (line 114) | func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh boo...
method updateServiceStats (line 174) | func (sm *systemdManager) updateServiceStats(conn *dbus.Conn, unit dbu...
method getServiceDetails (line 236) | func (sm *systemdManager) getServiceDetails(serviceName string) (syste...
function isSystemdAvailable (line 34) | func isSystemdAvailable() bool {
function newSystemdManager (line 52) | func newSystemdManager() (*systemdManager, error) {
function unescapeServiceName (line 282) | func unescapeServiceName(name string) string {
function getServicePatterns (line 296) | func getServicePatterns() []string {
FILE: agent/systemd_nonlinux.go
type systemdManager (line 12) | type systemdManager struct
method getServiceStats (line 22) | func (sm *systemdManager) getServiceStats(conn any, refresh bool) []*s...
method getServiceStatsCount (line 27) | func (sm *systemdManager) getServiceStatsCount() int {
method getFailedServiceCount (line 32) | func (sm *systemdManager) getFailedServiceCount() uint16 {
method getServiceDetails (line 36) | func (sm *systemdManager) getServiceDetails(string) (systemd.ServiceDe...
function newSystemdManager (line 17) | func newSystemdManager() (*systemdManager, error) {
FILE: agent/systemd_nonlinux_test.go
function TestNewSystemdManager (line 11) | func TestNewSystemdManager(t *testing.T) {
function TestSystemdManagerGetServiceStats (line 17) | func TestSystemdManagerGetServiceStats(t *testing.T) {
function TestSystemdManagerGetServiceDetails (line 30) | func TestSystemdManagerGetServiceDetails(t *testing.T) {
function TestSystemdManagerFields (line 46) | func TestSystemdManagerFields(t *testing.T) {
FILE: agent/systemd_test.go
function TestUnescapeServiceName (line 13) | func TestUnescapeServiceName(t *testing.T) {
function TestUnescapeServiceNameInvalid (line 35) | func TestUnescapeServiceNameInvalid(t *testing.T) {
function TestIsSystemdAvailable (line 52) | func TestIsSystemdAvailable(t *testing.T) {
function TestGetServicePatterns (line 81) | func TestGetServicePatterns(t *testing.T) {
FILE: agent/tools/fetchsmartctl/main.go
function main (line 21) | func main() {
function downloadFile (line 44) | func downloadFile(url, dest, shaHex string) error {
function fatalf (line 127) | func fatalf(format string, a ...any) {
FILE: agent/update.go
type restarter (line 13) | type restarter interface
type systemdRestarter (line 17) | type systemdRestarter struct
method Restart (line 19) | func (s *systemdRestarter) Restart() error {
type openRCRestarter (line 28) | type openRCRestarter struct
method Restart (line 30) | func (o *openRCRestarter) Restart() error {
type openWRTRestarter (line 38) | type openWRTRestarter struct
method Restart (line 40) | func (w *openWRTRestarter) Restart() error {
type freeBSDRestarter (line 49) | type freeBSDRestarter struct
method Restart (line 51) | func (f *freeBSDRestarter) Restart() error {
function detectRestarter (line 59) | func detectRestarter() restarter {
function Update (line 79) | func Update(useMirror bool) error {
FILE: agent/utils/utils.go
function GetEnv (line 12) | func GetEnv(key string) (value string, exists bool) {
function BytesToMegabytes (line 20) | func BytesToMegabytes(b float64) float64 {
function BytesToGigabytes (line 25) | func BytesToGigabytes(b uint64) float64 {
function TwoDecimals (line 30) | func TwoDecimals(value float64) float64 {
function ReadStringFile (line 40) | func ReadStringFile(path string) string {
function ReadStringFileOK (line 46) | func ReadStringFileOK(path string) (string, bool) {
function ReadStringFileLimited (line 56) | func ReadStringFileLimited(path string, maxSize int) (string, error) {
function FileExists (line 72) | func FileExists(path string) bool {
function ReadUintFile (line 78) | func ReadUintFile(path string) (uint64, bool) {
FILE: agent/utils/utils_test.go
function TestTwoDecimals (line 11) | func TestTwoDecimals(t *testing.T) {
function TestBytesToMegabytes (line 33) | func TestBytesToMegabytes(t *testing.T) {
function TestBytesToGigabytes (line 53) | func TestBytesToGigabytes(t *testing.T) {
function TestFileFunctions (line 73) | func TestFileFunctions(t *testing.T) {
function TestReadUintFile (line 105) | func TestReadUintFile(t *testing.T) {
function TestGetEnv (line 132) | func TestGetEnv(t *testing.T) {
FILE: agent/zfs/zfs_freebsd.go
function ARCSize (line 9) | func ARCSize() (uint64, error) {
FILE: agent/zfs/zfs_linux.go
function ARCSize (line 14) | func ARCSize() (uint64, error) {
FILE: agent/zfs/zfs_unsupported.go
function ARCSize (line 7) | func ARCSize() (uint64, error) {
FILE: beszel.go
constant Version (line 9) | Version = "0.18.4"
constant AppName (line 11) | AppName = "beszel"
FILE: internal/alerts/alerts.go
type hubLike (line 17) | type hubLike interface
type AlertManager (line 22) | type AlertManager struct
method bindEvents (line 109) | func (am *AlertManager) bindEvents() {
method IsNotificationSilenced (line 129) | func (am *AlertManager) IsNotificationSilenced(userID, systemID string...
method SendAlert (line 196) | func (am *AlertManager) SendAlert(data AlertMessageData) error {
method SendShoutrrrAlert (line 251) | func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, mess...
method SendTestNotification (line 305) | func (am *AlertManager) SendTestNotification(e *core.RequestEvent) err...
method setAlertTriggered (line 321) | func (am *AlertManager) setAlertTriggered(alert CachedAlertData, trigg...
type AlertMessageData (line 29) | type AlertMessageData struct
type UserNotificationSettings (line 38) | type UserNotificationSettings struct
type SystemAlertFsStats (line 43) | type SystemAlertFsStats struct
type SystemAlertStats (line 49) | type SystemAlertStats struct
type SystemAlertGPUData (line 61) | type SystemAlertGPUData struct
type SystemAlertData (line 65) | type SystemAlertData struct
function NewAlertManager (line 99) | func NewAlertManager(app hubLike) *AlertManager {
FILE: internal/alerts/alerts_api.go
function UpsertUserAlerts (line 14) | func UpsertUserAlerts(e *core.RequestEvent) error {
function DeleteUserAlerts (line 77) | func DeleteUserAlerts(e *core.RequestEvent) error {
FILE: internal/alerts/alerts_battery_test.go
function TestBatteryAlertLogic (line 21) | func TestBatteryAlertLogic(t *testing.T) {
function TestBatteryAlertNoBattery (line 170) | func TestBatteryAlertNoBattery(t *testing.T) {
function TestBatteryAlertAveragedSamples (line 228) | func TestBatteryAlertAveragedSamples(t *testing.T) {
FILE: internal/alerts/alerts_cache.go
type CachedAlertData (line 10) | type CachedAlertData struct
method PopulateFromRecord (line 21) | func (a *CachedAlertData) PopulateFromRecord(record *core.Record) {
type AlertsCache (line 33) | type AlertsCache struct
method bindEvents (line 49) | func (c *AlertsCache) bindEvents() *AlertsCache {
method PopulateFromDB (line 67) | func (c *AlertsCache) PopulateFromDB(force bool) error {
method Update (line 84) | func (c *AlertsCache) Update(record *core.Record) {
method Delete (line 100) | func (c *AlertsCache) Delete(record *core.Record) {
method GetSystemAlerts (line 111) | func (c *AlertsCache) GetSystemAlerts(systemID string) []CachedAlertDa...
method GetAlert (line 136) | func (c *AlertsCache) GetAlert(systemID, alertID string) (CachedAlertD...
method GetAlertsByName (line 144) | func (c *AlertsCache) GetAlertsByName(systemID, alertName string) []Ca...
method GetAlertsExcludingNames (line 156) | func (c *AlertsCache) GetAlertsExcludingNames(systemID string, exclude...
method Refresh (line 172) | func (c *AlertsCache) Refresh(alert CachedAlertData) (CachedAlertData,...
function NewAlertsCache (line 40) | func NewAlertsCache(app core.App) *AlertsCache {
FILE: internal/alerts/alerts_cache_test.go
function TestSystemAlertsCachePopulateAndFilter (line 14) | func TestSystemAlertsCachePopulateAndFilter(t *testing.T) {
function TestSystemAlertsCacheLazyLoadUpdateAndDelete (line 65) | func TestSystemAlertsCacheLazyLoadUpdateAndDelete(t *testing.T) {
function TestSystemAlertsCacheRefreshReturnsLatestCopy (line 103) | func TestSystemAlertsCacheRefreshReturnsLatestCopy(t *testing.T) {
function TestAlertManagerCacheLifecycle (line 137) | func TestAlertManagerCacheLifecycle(t *testing.T) {
FILE: internal/alerts/alerts_disk_test.go
function TestDiskAlertExtraFsMultiMinute (line 21) | func TestDiskAlertExtraFsMultiMinute(t *testing.T) {
FILE: internal/alerts/alerts_history.go
function resolveHistoryOnAlertDelete (line 11) | func resolveHistoryOnAlertDelete(e *core.RecordEvent) error {
function updateHistoryOnAlertUpdate (line 20) | func updateHistoryOnAlertUpdate(e *core.RecordEvent) error {
function resolveAlertHistoryRecord (line 44) | func resolveAlertHistoryRecord(app core.App, alertRecordID string) error {
function createAlertHistoryRecord (line 58) | func createAlertHistoryRecord(app core.App, alertRecord *core.Record) (a...
FILE: internal/alerts/alerts_quiet_hours_test.go
function TestAlertSilencedOneTime (line 17) | func TestAlertSilencedOneTime(t *testing.T) {
function TestAlertSilencedDaily (line 98) | func TestAlertSilencedDaily(t *testing.T) {
function TestAlertSilencedDailyMidnightCrossing (line 162) | func TestAlertSilencedDailyMidnightCrossing(t *testing.T) {
function TestAlertSilencedGlobal (line 204) | func TestAlertSilencedGlobal(t *testing.T) {
function TestAlertSilencedSystemSpecific (line 241) | func TestAlertSilencedSystemSpecific(t *testing.T) {
function TestAlertSilencedMultiUser (line 278) | func TestAlertSilencedMultiUser(t *testing.T) {
function TestAlertSilencedWithActualAlert (line 324) | func TestAlertSilencedWithActualAlert(t *testing.T) {
function TestAlertSilencedNoWindows (line 409) | func TestAlertSilencedNoWindows(t *testing.T) {
FILE: internal/alerts/alerts_smart.go
method handleSmartDeviceAlert (line 12) | func (am *AlertManager) handleSmartDeviceAlert(e *core.RecordEvent) error {
function shouldSendSmartDeviceAlert (line 69) | func shouldSendSmartDeviceAlert(oldState, newState string) bool {
function smartStateSeverity (line 78) | func smartStateSeverity(state string) int {
function smartStateEmoji (line 91) | func smartStateEmoji(state string) string {
function smartStateLabel (line 100) | func smartStateLabel(state string) string {
FILE: internal/alerts/alerts_smart_test.go
function TestSmartDeviceAlert (line 13) | func TestSmartDeviceAlert(t *testing.T) {
function TestSmartDeviceAlertPassedToWarning (line 60) | func TestSmartDeviceAlertPassedToWarning(t *testing.T) {
function TestSmartDeviceAlertWarningToFailed (line 94) | func TestSmartDeviceAlertWarningToFailed(t *testing.T) {
function TestSmartDeviceAlertNoAlertOnNonPassedToFailed (line 128) | func TestSmartDeviceAlertNoAlertOnNonPassedToFailed(t *testing.T) {
function TestSmartDeviceAlertMultipleUsers (line 179) | func TestSmartDeviceAlertMultipleUsers(t *testing.T) {
function TestSmartDeviceAlertWithoutModel (line 226) | func TestSmartDeviceAlertWithoutModel(t *testing.T) {
FILE: internal/alerts/alerts_status.go
type alertInfo (line 11) | type alertInfo struct
method Stop (line 19) | func (am *AlertManager) Stop() {
method HandleStatusAlerts (line 33) | func (am *AlertManager) HandleStatusAlerts(newStatus string, systemRecor...
method handleSystemDown (line 53) | func (am *AlertManager) handleSystemDown(systemName string, alerts []Cac...
method schedulePendingStatusAlert (line 62) | func (am *AlertManager) schedulePendingStatusAlert(systemName string, al...
method handleSystemUp (line 83) | func (am *AlertManager) handleSystemUp(systemName string, alerts []Cache...
method cancelPendingAlert (line 99) | func (am *AlertManager) cancelPendingAlert(alertID string) bool {
method processPendingAlert (line 113) | func (am *AlertManager) processPendingAlert(alertID string) {
method sendStatusAlert (line 130) | func (am *AlertManager) sendStatusAlert(alertStatus string, systemName s...
function resolveStatusAlerts (line 162) | func resolveStatusAlerts(app core.App) error {
method restorePendingStatusAlerts (line 194) | func (am *AlertManager) restorePendingStatusAlerts() error {
FILE: internal/alerts/alerts_status_test.go
function setStatusAlertEmail (line 18) | func setStatusAlertEmail(t *testing.T, hub core.App, userID, email strin...
function TestStatusAlerts (line 31) | func TestStatusAlerts(t *testing.T) {
function TestStatusAlertRecoveryBeforeDeadline (line 111) | func TestStatusAlertRecoveryBeforeDeadline(t *testing.T) {
function TestStatusAlertNormalRecovery (line 157) | func TestStatusAlertNormalRecovery(t *testing.T) {
function TestHandleStatusAlertsDoesNotSendRecoveryWhileDownIsOnlyPending (line 193) | func TestHandleStatusAlertsDoesNotSendRecoveryWhileDownIsOnlyPending(t *...
function TestStatusAlertTimerCancellationPreventsBoundaryDelivery (line 236) | func TestStatusAlertTimerCancellationPreventsBoundaryDelivery(t *testing...
function TestStatusAlertDownFiresAfterDelayExpires (line 288) | func TestStatusAlertDownFiresAfterDelayExpires(t *testing.T) {
function TestStatusAlertMultipleUsersRespectDifferentMinutes (line 338) | func TestStatusAlertMultipleUsersRespectDifferentMinutes(t *testing.T) {
function TestStatusAlertMultipleUsersRecoveryBetweenMinutesOnlyAlertsEarlierUser (line 425) | func TestStatusAlertMultipleUsersRecoveryBetweenMinutesOnlyAlertsEarlier...
function TestStatusAlertDuplicateDownCallIsIdempotent (line 513) | func TestStatusAlertDuplicateDownCallIsIdempotent(t *testing.T) {
function TestStatusAlertNoAlertRecord (line 550) | func TestStatusAlertNoAlertRecord(t *testing.T) {
function TestRestorePendingStatusAlertsRequeuesDownSystemsAfterRestart (line 574) | func TestRestorePendingStatusAlertsRequeuesDownSystemsAfterRestart(t *te...
function TestRestorePendingStatusAlertsSkipsNonDownOrAlreadyTriggeredAlerts (line 614) | func TestRestorePendingStatusAlertsSkipsNonDownOrAlreadyTriggeredAlerts(...
function TestRestorePendingStatusAlertsIsIdempotent (line 663) | func TestRestorePendingStatusAlertsIsIdempotent(t *testing.T) {
function TestResolveStatusAlertsFixesStaleTriggered (line 692) | func TestResolveStatusAlertsFixesStaleTriggered(t *testing.T) {
function TestResolveStatusAlerts (line 718) | func TestResolveStatusAlerts(t *testing.T) {
function TestAlertsHistoryStatus (line 818) | func TestAlertsHistoryStatus(t *testing.T) {
function TestStatusAlertClearedBeforeSend (line 884) | func TestStatusAlertClearedBeforeSend(t *testing.T) {
FILE: internal/alerts/alerts_system.go
method HandleSystemAlerts (line 16) | func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, da...
method sendSystemAlert (line 300) | func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
function isLowAlert (line 357) | func isLowAlert(name string) bool {
FILE: internal/alerts/alerts_system_test.go
type systemAlertValueSetter (line 16) | type systemAlertValueSetter
type systemAlertTestFixture (line 18) | type systemAlertTestFixture struct
method cleanup (line 74) | func (fixture *systemAlertTestFixture) cleanup() {
method assertTriggered (line 83) | func (fixture *systemAlertTestFixture) assertTriggered(t *testing.T, t...
function createCombinedData (line 24) | func createCombinedData[T any](value T, setValue systemAlertValueSetter[...
function newSystemAlertTestFixture (line 30) | func newSystemAlertTestFixture(t *testing.T, alertName string, min int, ...
function submitValue (line 78) | func submitValue[T any](fixture *systemAlertTestFixture, t *testing.T, v...
function waitForSystemAlert (line 91) | func waitForSystemAlert(d time.Duration) {
function testOneMinuteSystemAlert (line 96) | func testOneMinuteSystemAlert[T any](t *testing.T, alertName string, thr...
function testMultiMinuteSystemAlert (line 119) | func testMultiMinuteSystemAlert[T any](t *testing.T, alertName string, t...
function setCPUAlertValue (line 146) | func setCPUAlertValue(info *system.Info, stats *system.Stats, value floa...
function setMemoryAlertValue (line 151) | func setMemoryAlertValue(info *system.Info, stats *system.Stats, value f...
function setDiskAlertValue (line 156) | func setDiskAlertValue(info *system.Info, stats *system.Stats, value flo...
function setBandwidthAlertValue (line 161) | func setBandwidthAlertValue(info *system.Info, stats *system.Stats, valu...
function megabytesToBytes (line 166) | func megabytesToBytes(mb uint64) uint64 {
function setGPUAlertValue (line 170) | func setGPUAlertValue(info *system.Info, stats *system.Stats, value floa...
function setTemperatureAlertValue (line 177) | func setTemperatureAlertValue(info *system.Info, stats *system.Stats, va...
function setLoadAvgAlertValue (line 184) | func setLoadAvgAlertValue(info *system.Info, stats *system.Stats, value ...
function setBatteryAlertValue (line 189) | func setBatteryAlertValue(info *system.Info, stats *system.Stats, value ...
function TestSystemAlertsOneMin (line 194) | func TestSystemAlertsOneMin(t *testing.T) {
function TestSystemAlertsTwoMin (line 207) | func TestSystemAlertsTwoMin(t *testing.T) {
FILE: internal/alerts/alerts_test.go
function jsonReader (line 25) | func jsonReader(v any) io.Reader {
function TestUserAlertsApi (line 33) | func TestUserAlertsApi(t *testing.T) {
function TestAlertsHistory (line 372) | func TestAlertsHistory(t *testing.T) {
function TestSetAlertTriggered (line 501) | func TestSetAlertTriggered(t *testing.T) {
FILE: internal/alerts/alerts_test_helpers.go
function NewTestAlertManagerWithoutWorker (line 12) | func NewTestAlertManagerWithoutWorker(app hubLike) *AlertManager {
method GetSystemAlertsCache (line 20) | func (am *AlertManager) GetSystemAlertsCache() *AlertsCache {
method GetAlertManager (line 24) | func (am *AlertManager) GetAlertManager() *AlertManager {
method GetPendingAlerts (line 28) | func (am *AlertManager) GetPendingAlerts() *sync.Map {
method GetPendingAlertsCount (line 32) | func (am *AlertManager) GetPendingAlertsCount() int {
method ProcessPendingAlerts (line 42) | func (am *AlertManager) ProcessPendingAlerts() ([]CachedAlertData, error) {
method ForceExpirePendingAlerts (line 61) | func (am *AlertManager) ForceExpirePendingAlerts() {
method ResetPendingAlertTimer (line 70) | func (am *AlertManager) ResetPendingAlertTimer(alertID string, delay tim...
function ResolveStatusAlerts (line 87) | func ResolveStatusAlerts(app core.App) error {
method RestorePendingStatusAlerts (line 91) | func (am *AlertManager) RestorePendingStatusAlerts() error {
method SetAlertTriggered (line 95) | func (am *AlertManager) SetAlertTriggered(alert CachedAlertData, trigger...
FILE: internal/cmd/agent/agent.go
type cmdOptions (line 18) | type cmdOptions struct
method parse (line 27) | func (opts *cmdOptions) parse() bool {
method loadPublicKeys (line 113) | func (opts *cmdOptions) loadPublicKeys() ([]ssh.PublicKey, error) {
method getAddress (line 137) | func (opts *cmdOptions) getAddress() string {
function handleFingerprint (line 142) | func handleFingerprint() {
function fingerprintUsage (line 169) | func fingerprintUsage() string {
function main (line 173) | func main() {
FILE: internal/cmd/agent/agent_test.go
function TestGetAddress (line 17) | func TestGetAddress(t *testing.T) {
function TestLoadPublicKeys (line 95) | func TestLoadPublicKeys(t *testing.T) {
function TestGetNetwork (line 190) | func TestGetNetwork(t *testing.T) {
function TestParseFlags (line 244) | func TestParseFlags(t *testing.T) {
FILE: internal/cmd/hub/hub.go
function main (line 19) | func main() {
function getBaseApp (line 38) | func getBaseApp() *pocketbase.PocketBase {
function newHealthCmd (line 68) | func newHealthCmd() *cobra.Command {
function checkHealth (line 87) | func checkHealth(baseURL string) error {
FILE: internal/common/common-ws.go
constant GetData (line 14) | GetData WebSocketAction = iota
constant CheckFingerprint (line 16) | CheckFingerprint
constant GetContainerLogs (line 18) | GetContainerLogs
constant GetContainerInfo (line 20) | GetContainerInfo
constant GetSmartData (line 22) | GetSmartData
constant GetSystemdInfo (line 24) | GetSystemdInfo
type HubRequest (line 29) | type HubRequest struct
type AgentResponse (line 36) | type AgentResponse struct
type FingerprintRequest (line 48) | type FingerprintRequest struct
type FingerprintResponse (line 53) | type FingerprintResponse struct
type DataRequestOptions (line 61) | type DataRequestOptions struct
type ContainerLogsRequest (line 66) | type ContainerLogsRequest struct
type ContainerInfoRequest (line 70) | type ContainerInfoRequest struct
type SystemdInfoRequest (line 74) | type SystemdInfoRequest struct
FILE: internal/entities/container/container.go
type ApiInfo (line 6) | type ApiInfo struct
type ApiStats (line 38) | type ApiStats struct
method CalculateCpuPercentLinux (line 54) | func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, p...
method CalculateCpuPercentWindows (line 67) | func (s *ApiStats) CalculateCpuPercentWindows(prevCpuUsage uint64, pre...
type HostInfo (line 47) | type HostInfo struct
type CPUStats (line 83) | type CPUStats struct
type CPUUsage (line 90) | type CPUUsage struct
type MemoryStats (line 97) | type MemoryStats struct
type MemoryStatsStats (line 106) | type MemoryStatsStats struct
type NetworkStats (line 111) | type NetworkStats struct
type prevNetStats (line 118) | type prevNetStats struct
constant DockerHealthNone (line 126) | DockerHealthNone DockerHealth = iota
constant DockerHealthStarting (line 127) | DockerHealthStarting
constant DockerHealthHealthy (line 128) | DockerHealthHealthy
constant DockerHealthUnhealthy (line 129) | DockerHealthUnhealthy
type Stats (line 140) | type Stats struct
FILE: internal/entities/smart/smart.go
type VersionInfo (line 10) | type VersionInfo
type SmartctlInfo (line 12) | type SmartctlInfo struct
type DeviceInfo (line 21) | type DeviceInfo struct
type UserCapacity (line 28) | type UserCapacity struct
type SmartStatusInfo (line 76) | type SmartStatusInfo struct
type StatusInfo (line 80) | type StatusInfo struct
type PollingMinutes (line 86) | type PollingMinutes struct
type CapabilitiesInfo (line 91) | type CapabilitiesInfo struct
type SummaryInfo (line 127) | type SummaryInfo struct
type AtaSmartAttributes (line 132) | type AtaSmartAttributes struct
type AtaDeviceStatistics (line 136) | type AtaDeviceStatistics struct
type AtaDeviceStatisticsPage (line 140) | type AtaDeviceStatisticsPage struct
type AtaDeviceStatisticsEntry (line 145) | type AtaDeviceStatisticsEntry struct
type AtaSmartAttribute (line 150) | type AtaSmartAttribute struct
type RawValue (line 172) | type RawValue struct
method UnmarshalJSON (line 177) | func (r *RawValue) UnmarshalJSON(data []byte) error {
type SmartRawValue (line 204) | type SmartRawValue
method UnmarshalJSON (line 207) | func (v *SmartRawValue) UnmarshalJSON(data []byte) error {
function ParseSmartRawValueString (line 246) | func ParseSmartRawValueString(value string) (uint64, bool) {
type TemperatureInfo (line 287) | type TemperatureInfo struct
type TemperatureInfoScsi (line 291) | type TemperatureInfoScsi struct
type SmartctlInfoLegacy (line 325) | type SmartctlInfoLegacy struct
type SmartInfoForSata (line 334) | type SmartInfoForSata struct
type ScsiErrorCounter (line 369) | type ScsiErrorCounter struct
type ScsiErrorCounterLog (line 379) | type ScsiErrorCounterLog struct
type ScsiStartStopCycleCounter (line 385) | type ScsiStartStopCycleCounter struct
type PowerOnTimeScsi (line 394) | type PowerOnTimeScsi struct
type SmartInfoForScsi (line 399) | type SmartInfoForScsi struct
type SmartctlInfoNvme (line 425) | type SmartctlInfoNvme struct
type SmartStatusInfoNvme (line 458) | type SmartStatusInfoNvme struct
type SmartStatusNVMe (line 463) | type SmartStatusNVMe struct
type NVMeSmartHealthInformationLog (line 467) | type NVMeSmartHealthInformationLog struct
type SmartInfoForNvme (line 488) | type SmartInfoForNvme struct
type TemperatureInfoNvme (line 513) | type TemperatureInfoNvme struct
type PowerOnTimeInfoNvme (line 517) | type PowerOnTimeInfoNvme struct
type SmartData (line 521) | type SmartData struct
type SmartAttribute (line 534) | type SmartAttribute struct
FILE: internal/entities/smart/smart_test.go
function TestSmartRawValueUnmarshalDuration (line 10) | func TestSmartRawValueUnmarshalDuration(t *testing.T) {
function TestSmartRawValueUnmarshalNumericString (line 19) | func TestSmartRawValueUnmarshalNumericString(t *testing.T) {
function TestSmartRawValueUnmarshalParenthetical (line 28) | func TestSmartRawValueUnmarshalParenthetical(t *testing.T) {
function TestSmartRawValueUnmarshalDurationWithFractions (line 37) | func TestSmartRawValueUnmarshalDurationWithFractions(t *testing.T) {
function TestSmartRawValueUnmarshalParentheticalRawValue (line 46) | func TestSmartRawValueUnmarshalParentheticalRawValue(t *testing.T) {
function TestSmartRawValueUnmarshalDurationRawValue (line 55) | func TestSmartRawValueUnmarshalDurationRawValue(t *testing.T) {
FILE: internal/entities/system/system.go
type Stats (line 13) | type Stats struct
type Uint8Slice (line 56) | type Uint8Slice
method MarshalJSON (line 58) | func (s Uint8Slice) MarshalJSON() ([]byte, error) {
type GPUData (line 70) | type GPUData struct
type FsStats (line 82) | type FsStats struct
type NetIoStats (line 102) | type NetIoStats struct
constant Linux (line 112) | Linux Os = iota
constant Darwin (line 113) | Darwin
constant Windows (line 114) | Windows
constant Freebsd (line 115) | Freebsd
constant ConnectionTypeNone (line 121) | ConnectionTypeNone ConnectionType = iota
constant ConnectionTypeSSH (line 122) | ConnectionTypeSSH
constant ConnectionTypeWebSocket (line 123) | ConnectionTypeWebSocket
type Info (line 127) | type Info struct
type Details (line 157) | type Details struct
type CombinedData (line 172) | type CombinedData struct
FILE: internal/entities/systemd/systemd.go
type ServiceState (line 10) | type ServiceState
constant StatusActive (line 13) | StatusActive ServiceState = iota
constant StatusInactive (line 14) | StatusInactive
constant StatusFailed (line 15) | StatusFailed
constant StatusActivating (line 16) | StatusActivating
constant StatusDeactivating (line 17) | StatusDeactivating
constant StatusReloading (line 18) | StatusReloading
type ServiceSubState (line 22) | type ServiceSubState
constant SubStateDead (line 25) | SubStateDead ServiceSubState = iota
constant SubStateRunning (line 26) | SubStateRunning
constant SubStateExited (line 27) | SubStateExited
constant SubStateFailed (line 28) | SubStateFailed
constant SubStateUnknown (line 29) | SubStateUnknown
function ParseServiceStatus (line 33) | func ParseServiceStatus(status string) ServiceState {
function ParseServiceSubState (line 53) | func ParseServiceSubState(subState string) ServiceSubState {
type Service (line 69) | type Service struct
method UpdateCPUPercent (line 82) | func (s *Service) UpdateCPUPercent(cpuUsage uint64) {
function twoDecimals (line 114) | func twoDecimals(value float64) float64 {
type ServiceDependency (line 119) | type ServiceDependency struct
type ServiceDetails (line 127) | type ServiceDetails
FILE: internal/entities/systemd/systemd_test.go
function TestParseServiceStatus (line 13) | func TestParseServiceStatus(t *testing.T) {
function TestParseServiceSubState (line 36) | func TestParseServiceSubState(t *testing.T) {
function TestServiceUpdateCPUPercent (line 58) | func TestServiceUpdateCPUPercent(t *testing.T) {
FILE: internal/ghupdate/extract.go
function extract (line 16) | func extract(srcPath, destDir string) error {
function extractTarGz (line 25) | func extractTarGz(srcPath, destDir string) error {
function extractZip (line 80) | func extractZip(src, dest string) error {
function extractFile (line 102) | func extractFile(zipFile *zip.File, basePath string) error {
FILE: internal/ghupdate/ghupdate.go
constant colorReset (line 26) | colorReset = "\033[0m"
constant ColorYellow (line 27) | ColorYellow = "\033[33m"
constant ColorGreen (line 28) | ColorGreen = "\033[32m"
constant colorCyan (line 29) | colorCyan = "\033[36m"
constant colorGray (line 30) | colorGray = "\033[90m"
function ColorPrint (line 33) | func ColorPrint(color, text string) {
function ColorPrintf (line 37) | func ColorPrintf(color, format string, args ...any) {
type HttpClient (line 42) | type HttpClient interface
type Config (line 49) | type Config struct
type updater (line 75) | type updater struct
method update (line 89) | func (p *updater) update() (updated bool, err error) {
function Update (line 80) | func Update(config Config) (updated bool, err error) {
function fetchLatestRelease (line 229) | func fetchLatestRelease(
function downloadFile (line 267) | func downloadFile(
function isCrossDeviceError (line 312) | func isCrossDeviceError(err error) bool {
function copyFile (line 318) | func copyFile(src, dst string) error {
function archiveSuffix (line 345) | func archiveSuffix(binaryName, goos, goarch string) string {
function isGlibc (line 356) | func isGlibc() bool {
FILE: internal/ghupdate/ghupdate_test.go
function TestReleaseFindAssetBySuffix (line 8) | func TestReleaseFindAssetBySuffix(t *testing.T) {
function TestExtractFailure (line 28) | func TestExtractFailure(t *testing.T) {
FILE: internal/ghupdate/release.go
type releaseAsset (line 8) | type releaseAsset struct
type release (line 15) | type release struct
method findAssetBySuffix (line 26) | func (r *release) findAssetBySuffix(suffix string) (*releaseAsset, err...
FILE: internal/ghupdate/selinux.go
function HandleSELinuxContext (line 10) | func HandleSELinuxContext(path string) error {
function trySemanageRestorecon (line 41) | func trySemanageRestorecon(path string) bool {
FILE: internal/ghupdate/selinux_test.go
function TestHandleSELinuxContext_NoSELinux (line 10) | func TestHandleSELinuxContext_NoSELinux(t *testing.T) {
function TestHandleSELinuxContext_InvalidPath (line 29) | func TestHandleSELinuxContext_InvalidPath(t *testing.T) {
function TestTrySemanageRestorecon_NoTools (line 42) | func TestTrySemanageRestorecon_NoTools(t *testing.T) {
FILE: internal/hub/agent_connect.go
type agentConnectRequest (line 23) | type agentConnectRequest struct
method agentConnect (line 59) | func (acr *agentConnectRequest) agentConnect() (err error) {
method verifyWsConn (line 105) | func (acr *agentConnectRequest) verifyWsConn(conn *gws.Conn, fpRecords...
method validateAgentHeaders (line 140) | func (acr *agentConnectRequest) validateAgentHeaders(headers http.Head...
method sendResponseError (line 151) | func (acr *agentConnectRequest) sendResponseError(res http.ResponseWri...
method findOrCreateSystemForToken (line 173) | func (acr *agentConnectRequest) findOrCreateSystemForToken(fpRecords [...
method handleNoRecords (line 190) | func (acr *agentConnectRequest) handleNoRecords(agentFingerprint commo...
method handleSingleRecord (line 202) | func (acr *agentConnectRequest) handleSingleRecord(fpRecord ws.Fingerp...
method handleMultipleRecordsOrUniversalToken (line 223) | func (acr *agentConnectRequest) handleMultipleRecordsOrUniversalToken(...
method createNewSystemForUniversalToken (line 241) | func (acr *agentConnectRequest) createNewSystemForUniversalToken(agent...
method createSystem (line 267) | func (acr *agentConnectRequest) createSystem(agentFingerprint common.F...
type tokenMap (line 38) | type tokenMap struct
method GetMap (line 44) | func (tm *tokenMap) GetMap() *expirymap.ExpiryMap[string] {
method handleAgentConnect (line 52) | func (h *Hub) handleAgentConnect(e *core.RequestEvent) error {
function getFingerprintRecordsByToken (line 160) | func getFingerprintRecordsByToken(token string, h *Hub) []ws.Fingerprint...
method SetFingerprint (line 294) | func (h *Hub) SetFingerprint(fpRecord *ws.FingerprintRecord, fingerprint...
function getRealIP (line 316) | func getRealIP(r *http.Request) string {
FILE: internal/hub/agent_connect_test.go
function createTestHub (line 29) | func createTestHub(t testing.TB) (*Hub, *pbtests.TestApp, error) {
function cleanupTestHub (line 39) | func cleanupTestHub(hub *Hub, testApp *pbtests.TestApp) {
function createTestRecord (line 59) | func createTestRecord(app core.App, collection string, data map[string]a...
function createTestUser (line 73) | func createTestUser(app core.App) (*core.Record, error) {
function TestValidateAgentHeaders (line 82) | func TestValidateAgentHeaders(t *testing.T) {
function TestGetAllFingerprintRecordsByToken (line 163) | func TestGetAllFingerprintRecordsByToken(t *testing.T) {
function TestSetFingerprint (line 253) | func TestSetFingerprint(t *testing.T) {
function TestCreateSystemFromAgentData (line 333) | func TestCreateSystemFromAgentData(t *testing.T) {
function TestUniversalTokenFlow (line 443) | func TestUniversalTokenFlow(t *testing.T) {
function TestAgentConnect (line 511) | func TestAgentConnect(t *testing.T) {
function TestSendResponseError (line 633) | func TestSendResponseError(t *testing.T) {
function TestHandleAgentConnect (line 670) | func TestHandleAgentConnect(t *testing.T) {
function TestAgentWebSocketIntegration (line 756) | func TestAgentWebSocketIntegration(t *testing.T) {
function TestMultipleSystemsWithSameUniversalToken (line 997) | func TestMultipleSystemsWithSameUniversalToken(t *testing.T) {
function TestPermanentUniversalTokenFromDB (line 1199) | func TestPermanentUniversalTokenFromDB(t *testing.T) {
function TestFindOrCreateSystemForToken (line 1297) | func TestFindOrCreateSystemForToken(t *testing.T) {
function TestGetRealIP (line 1744) | func TestGetRealIP(t *testing.T) {
FILE: internal/hub/config/config.go
type config (line 19) | type config struct
type systemConfig (line 23) | type systemConfig struct
function SyncSystems (line 32) | func SyncSystems(e *core.ServeEvent) error {
function generateYAML (line 164) | func generateYAML(h core.App) (string, error) {
function getUserEmailMap (line 237) | func getUserEmailMap(h core.App, userIDs []string) (map[string]string, e...
function updateFingerprintToken (line 252) | func updateFingerprintToken(app core.App, systemID, token string) error {
function createFingerprintRecord (line 266) | func createFingerprintRecord(app core.App, systemID, token string) error {
function GetYamlConfig (line 281) | func GetYamlConfig(e *core.RequestEvent) error {
FILE: internal/hub/config/config_test.go
type testConfig (line 21) | type testConfig struct
type testSystemConfig (line 25) | type testSystemConfig struct
function createConfigTestFingerprint (line 51) | func createConfigTestFingerprint(app core.App, systemID, token, fingerpr...
function TestConfigSyncWithTokens (line 66) | func TestConfigSyncWithTokens(t *testing.T) {
function TestConfigMigrationScenario (line 191) | func TestConfigMigrationScenario(t *testing.T) {
FILE: internal/hub/expirymap/expirymap.go
type val (line 13) | type val struct
type ExpiryMap (line 18) | type ExpiryMap struct
function New (line 25) | func New[T comparable](cleanupInterval time.Duration) *ExpiryMap[T] {
method Set (line 35) | func (m *ExpiryMap[T]) Set(key string, value T, ttl time.Duration) {
method GetOk (line 44) | func (m *ExpiryMap[T]) GetOk(key string) (T, bool) {
method GetByValue (line 60) | func (m *ExpiryMap[T]) GetByValue(val T) (key string, value T, ok bool) {
method Remove (line 75) | func (m *ExpiryMap[T]) Remove(key string) {
method RemovebyValue (line 80) | func (m *ExpiryMap[T]) RemovebyValue(value T) (T, bool) {
method startCleaner (line 91) | func (m *ExpiryMap[T]) startCleaner(interval time.Duration) {
method StopCleaner (line 104) | func (m *ExpiryMap[T]) StopCleaner() {
method cleanup (line 111) | func (m *ExpiryMap[T]) cleanup() {
method UpdateExpiration (line 121) | func (m *ExpiryMap[T]) UpdateExpiration(key string, ttl time.Duration) {
FILE: internal/hub/expirymap/expirymap_test.go
method Has (line 17) | func (m *ExpiryMap[T]) Has(key string) bool {
method Get (line 23) | func (m *ExpiryMap[T]) Get(key string) T {
method Len (line 29) | func (m *ExpiryMap[T]) Len() int {
function TestExpiryMap_BasicOperations (line 40) | func TestExpiryMap_BasicOperations(t *testing.T) {
function TestExpiryMap_Expiration (line 62) | func TestExpiryMap_Expiration(t *testing.T) {
function TestExpiryMap_LazyCleanup (line 81) | func TestExpiryMap_LazyCleanup(t *testing.T) {
function TestExpiryMap_Len (line 102) | func TestExpiryMap_Len(t *testing.T) {
function TestExpiryMap_CustomInterval (line 123) | func TestExpiryMap_CustomInterval(t *testing.T) {
function TestExpiryMap_GenericTypes (line 141) | func TestExpiryMap_GenericTypes(t *testing.T) {
function TestExpiryMap_UpdateExpiration (line 181) | func TestExpiryMap_UpdateExpiration(t *testing.T) {
function TestExpiryMap_ZeroValues (line 208) | func TestExpiryMap_ZeroValues(t *testing.T) {
function TestExpiryMap_Concurrent (line 223) | func TestExpiryMap_Concurrent(t *testing.T) {
function TestExpiryMap_GetByValue (line 255) | func TestExpiryMap_GetByValue(t *testing.T) {
function TestExpiryMap_GetByValue_Expiration (line 282) | func TestExpiryMap_GetByValue_Expiration(t *testing.T) {
function TestExpiryMap_GetByValue_GenericTypes (line 311) | func TestExpiryMap_GetByValue_GenericTypes(t *testing.T) {
function TestExpiryMap_RemoveValue (line 356) | func TestExpiryMap_RemoveValue(t *testing.T) {
function TestExpiryMap_RemoveValue_GenericTypes (line 385) | func TestExpiryMap_RemoveValue_GenericTypes(t *testing.T) {
function TestExpiryMap_RemoveValue_WithExpiration (line 434) | func TestExpiryMap_RemoveValue_WithExpiration(t *testing.T) {
function TestExpiryMap_ValueOperations_Integration (line 462) | func TestExpiryMap_ValueOperations_Integration(t *testing.T) {
function TestExpiryMap_Cleaner (line 505) | func TestExpiryMap_Cleaner(t *testing.T) {
function TestExpiryMap_StopCleaner (line 527) | func TestExpiryMap_StopCleaner(t *testing.T) {
FILE: internal/hub/heartbeat/heartbeat.go
constant defaultInterval (line 22) | defaultInterval = 60
constant httpTimeout (line 23) | httpTimeout = 10 * time.Second
type Payload (line 27) | type Payload struct
type SystemsSummary (line 40) | type SystemsSummary struct
type SystemInfo (line 49) | type SystemInfo struct
type AlertInfo (line 56) | type AlertInfo struct
type Config (line 64) | type Config struct
type Heartbeat (line 71) | type Heartbeat struct
method Start (line 114) | func (hb *Heartbeat) Start(stop <-chan struct{}) {
method Send (line 139) | func (hb *Heartbeat) Send() error {
method GetConfig (line 144) | func (hb *Heartbeat) GetConfig() Config {
method send (line 148) | func (hb *Heartbeat) send() error {
method buildPayload (line 198) | func (hb *Heartbeat) buildPayload() (*Payload, error) {
function New (line 79) | func New(app core.App, getEnv func(string) (string, bool)) *Heartbeat {
function normalizeMethod (line 289) | func normalizeMethod(method string) string {
function sanitizeHeartbeatURL (line 297) | func sanitizeHeartbeatURL(rawURL string) string {
FILE: internal/hub/heartbeat/heartbeat_test.go
function TestNew (line 19) | func TestNew(t *testing.T) {
function TestSendGETDoesNotRequireAppOrDB (line 56) | func TestSendGETDoesNotRequireAppOrDB(t *testing.T) {
function TestSendReturnsErrorOnHTTPFailureStatus (line 74) | func TestSendReturnsErrorOnHTTPFailureStatus(t *testing.T) {
function TestSendPOSTBuildsExpectedStatuses (line 92) | func TestSendPOSTBuildsExpectedStatuses(t *testing.T) {
function newTestHub (line 210) | func newTestHub(t *testing.T) *beszeltests.TestHub {
function createTestUser (line 218) | func createTestUser(t *testing.T, app *beszeltests.TestHub) *core.Record {
function createTestSystem (line 225) | func createTestSystem(t *testing.T, app *beszeltests.TestHub, userID, na...
function createTriggeredAlert (line 238) | func createTriggeredAlert(t *testing.T, app *beszeltests.TestHub, userID...
function envGetter (line 252) | func envGetter(values map[string]string) func(string) (string, bool) {
FILE: internal/hub/hub.go
type Hub (line 32) | type Hub struct
method StartHub (line 73) | func (h *Hub) StartHub() error {
method initialize (line 125) | func (h *Hub) initialize(e *core.ServeEvent) error {
method registerCronJobs (line 242) | func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
method registerMiddlewares (line 251) | func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
method registerApiRoutes (line 282) | func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
method getUniversalToken (line 331) | func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
method getHeartbeatStatus (line 424) | func (h *Hub) getHeartbeatStatus(e *core.RequestEvent) error {
method testHeartbeat (line 444) | func (h *Hub) testHeartbeat(e *core.RequestEvent) error {
method containerRequestHandler (line 460) | func (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc ...
method getContainerLogs (line 485) | func (h *Hub) getContainerLogs(e *core.RequestEvent) error {
method getContainerInfo (line 491) | func (h *Hub) getContainerInfo(e *core.RequestEvent) error {
method getSystemdInfo (line 498) | func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
method refreshSmartData (line 520) | func (h *Hub) refreshSmartData(e *core.RequestEvent) error {
method GetSSHKey (line 540) | func (h *Hub) GetSSHKey(dataDir string) (ssh.Signer, error) {
method MakeLink (line 593) | func (h *Hub) MakeLink(parts ...string) string {
function NewHub (line 48) | func NewHub(app core.App) *Hub {
function GetEnv (line 65) | func GetEnv(key string) (value string, exists bool) {
function setCollectionAuthSettings (line 145) | func setCollectionAuthSettings(app core.App) error {
FILE: internal/hub/hub_test.go
function jsonReader (line 28) | func jsonReader(v any) io.Reader {
function TestMakeLink (line 36) | func TestMakeLink(t *testing.T) {
function TestGetSSHKey (line 126) | func TestGetSSHKey(t *testing.T) {
function TestApiRoutesAuthentication (line 268) | func TestApiRoutesAuthentication(t *testing.T) {
function TestFirstUserCreation (line 674) | func TestFirstUserCreation(t *testing.T) {
function TestCreateUserEndpointAvailability (line 783) | func TestCreateUserEndpointAvailability(t *testing.T) {
function TestAutoLoginMiddleware (line 851) | func TestAutoLoginMiddleware(t *testing.T) {
function TestTrustedHeaderMiddleware (line 905) | func TestTrustedHeaderMiddleware(t *testing.T) {
FILE: internal/hub/hub_test_helpers.go
method GetSystemManager (line 8) | func (h *Hub) GetSystemManager() *systems.SystemManager {
method GetPubkey (line 13) | func (h *Hub) GetPubkey() string {
method SetPubkey (line 18) | func (h *Hub) SetPubkey(pubkey string) {
FILE: internal/hub/server_development.go
type responseModifier (line 21) | type responseModifier struct
method RoundTrip (line 26) | func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Respon...
method modifyHTML (line 50) | func (rm *responseModifier) modifyHTML(html string) string {
method startServer (line 64) | func (h *Hub) startServer(se *core.ServeEvent) error {
FILE: internal/hub/server_production.go
method startServer (line 19) | func (h *Hub) startServer(se *core.ServeEvent) error {
FILE: internal/hub/systems/system.go
type System (line 34) | type System struct
method StartUpdater (line 65) | func (sys *System) StartUpdater() {
method update (line 118) | func (sys *System) update() error {
method handlePaused (line 172) | func (sys *System) handlePaused() {
method createRecords (line 186) | func (sys *System) createRecords(data *system.CombinedData) (*core.Rec...
method getRecord (line 346) | func (sys *System) getRecord() (*core.Record, error) {
method setDown (line 358) | func (sys *System) setDown(originalError error) error {
method getContext (line 373) | func (sys *System) getContext() (context.Context, context.CancelFunc) {
method request (line 382) | func (sys *System) request(ctx context.Context, action common.WebSocke...
method ensureSSHTransport (line 429) | func (sys *System) ensureSSHTransport() error {
method fetchDataFromAgent (line 452) | func (sys *System) fetchDataFromAgent(options common.DataRequestOption...
method fetchDataViaWebSocket (line 473) | func (sys *System) fetchDataViaWebSocket(options common.DataRequestOpt...
method FetchContainerInfoFromAgent (line 486) | func (sys *System) FetchContainerInfoFromAgent(containerID string) (st...
method FetchContainerLogsFromAgent (line 495) | func (sys *System) FetchContainerLogsFromAgent(containerID string) (st...
method FetchSystemdInfoFromAgent (line 504) | func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (syst...
method FetchSmartDataFromAgent (line 513) | func (sys *System) FetchSmartDataFromAgent() (map[string]smart.SmartDa...
method fetchDataViaSSH (line 532) | func (sys *System) fetchDataViaSSH(options common.DataRequestOptions) ...
method runSSHOperation (line 586) | func (sys *System) runSSHOperation(timeout time.Duration, retries int,...
method createSSHClient (line 627) | func (s *System) createSSHClient() error {
method createSessionWithTimeout (line 651) | func (sys *System) createSessionWithTimeout(timeout time.Duration) (*s...
method closeSSHConnection (line 681) | func (sys *System) closeSSHConnection() {
method closeWebSocketConnection (line 694) | func (sys *System) closeWebSocketConnection() {
method NewSystem (line 53) | func (sm *SystemManager) NewSystem(systemId string) *System {
function createSystemDetailsRecord (line 252) | func createSystemDetailsRecord(app core.App, data *system.Details, syste...
function createSystemdStatsRecords (line 277) | func createSystemdStatsRecords(app core.App, data []*systemd.Service, sy...
function createContainerRecords (line 309) | func createContainerRecords(app core.App, data []*container.Stats, syste...
function shouldFallbackToSSH (line 408) | func shouldFallbackToSSH(err error) bool {
function shouldCloseWebSocket (line 421) | func shouldCloseWebSocket(err error) bool {
function makeStableHashId (line 521) | func makeStableHashId(strings ...string) string {
function extractAgentVersion (line 701) | func extractAgentVersion(versionString string) (semver.Version, error) {
function getJitter (line 709) | func getJitter() <-chan time.Time {
function migrateDeprecatedFields (line 722) | func migrateDeprecatedFields(cd *system.CombinedData, createDetails bool) {
FILE: internal/hub/systems/system_manager.go
constant up (line 25) | up string = "up"
constant down (line 26) | down string = "down"
constant paused (line 27) | paused string = "paused"
constant pending (line 28) | pending string = "pending"
constant interval (line 31) | interval int = 60_000
constant sessionTimeout (line 35) | sessionTimeout = 4 * time.Second
type SystemManager (line 43) | type SystemManager struct
method GetSystem (line 70) | func (sm *SystemManager) GetSystem(systemID string) (*System, error) {
method Initialize (line 81) | func (sm *SystemManager) Initialize() error {
method bindEventHooks (line 114) | func (sm *SystemManager) bindEventHooks() {
method onTokenRotated (line 128) | func (sm *SystemManager) onTokenRotated(e *core.RecordEvent) error {
method onRecordCreate (line 145) | func (sm *SystemManager) onRecordCreate(e *core.RecordEvent) error {
method onRecordAfterCreateSuccess (line 153) | func (sm *SystemManager) onRecordAfterCreateSuccess(e *core.RecordEven...
method onRecordUpdate (line 162) | func (sm *SystemManager) onRecordUpdate(e *core.RecordEvent) error {
method onRecordAfterUpdateSuccess (line 176) | func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEven...
method onRecordAfterDeleteSuccess (line 230) | func (sm *SystemManager) onRecordAfterDeleteSuccess(e *core.RecordEven...
method AddSystem (line 238) | func (sm *SystemManager) AddSystem(sys *System) error {
method RemoveSystem (line 260) | func (sm *SystemManager) RemoveSystem(systemID string) error {
method AddRecord (line 282) | func (sm *SystemManager) AddRecord(record *core.Record, system *System...
method AddWebSocketSystem (line 304) | func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVers...
method createSSHClientConfig (line 321) | func (sm *SystemManager) createSSHClientConfig() error {
type hubLike (line 52) | type hubLike interface
function NewSystemManager (line 61) | func NewSystemManager(hub hubLike) *SystemManager {
function deactivateAlerts (line 346) | func deactivateAlerts(app core.App, systemID string) error {
FILE: internal/hub/systems/system_realtime.go
type subscriptionInfo (line 14) | type subscriptionInfo struct
method onRealtimeConnectRequest (line 29) | func (sm *SystemManager) onRealtimeConnectRequest(e *core.RealtimeConnec...
method onRealtimeSubscribeRequest (line 41) | func (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubs...
method onRealtimeSubscriptionAdded (line 74) | func (sm *SystemManager) onRealtimeSubscriptionAdded() {
method checkSubscriptions (line 94) | func (sm *SystemManager) checkSubscriptions() {
method removeRealtimeSubscription (line 121) | func (sm *SystemManager) removeRealtimeSubscription(subscription string,...
method startRealtimeWorker (line 136) | func (sm *SystemManager) startRealtimeWorker() {
method fetchRealtimeDataAndNotify (line 155) | func (sm *SystemManager) fetchRealtimeDataAndNotify() {
function notify (line 176) | func notify(app core.App, subscription string, data []byte) error {
FILE: internal/hub/systems/system_smart.go
method FetchAndSaveSmartDevices (line 13) | func (sys *System) FetchAndSaveSmartDevices() error {
method saveSmartDevices (line 22) | func (sys *System) saveSmartDevices(smartData map[string]smart.SmartData...
method upsertSmartDeviceRecord (line 42) | func (sys *System) upsertSmartDeviceRecord(collection *core.Collection, ...
function extractPowerMetrics (line 78) | func extractPowerMetrics(attributes []*smart.SmartAttribute) (powerOnHou...
FILE: internal/hub/systems/system_systemd_test.go
function TestGetSystemdServiceId (line 11) | func TestGetSystemdServiceId(t *testing.T) {
FILE: internal/hub/systems/system_test.go
function TestCombinedData_MigrateDeprecatedFields (line 11) | func TestCombinedData_MigrateDeprecatedFields(t *testing.T) {
FILE: internal/hub/systems/systems_production.go
function backgroundSmartFetchEnabled (line 9) | func backgroundSmartFetchEnabled() bool { return true }
FILE: internal/hub/systems/systems_test.go
function TestSystemManagerNew (line 21) | func TestSystemManagerNew(t *testing.T) {
function testOld (line 133) | func testOld(t *testing.T, hub *tests.TestHub) {
FILE: internal/hub/systems/systems_test_helpers.go
function backgroundSmartFetchEnabled (line 18) | func backgroundSmartFetchEnabled() bool { return false }
method GetSystemCount (line 21) | func (sm *SystemManager) GetSystemCount() int {
method HasSystem (line 26) | func (sm *SystemManager) HasSystem(systemID string) bool {
method GetSystemStatusFromStore (line 32) | func (sm *SystemManager) GetSystemStatusFromStore(systemID string) string {
method GetSystemContextFromStore (line 41) | func (sm *SystemManager) GetSystemContextFromStore(systemID string) (con...
method GetSystemFromStore (line 50) | func (sm *SystemManager) GetSystemFromStore(systemID string) (*System, e...
method GetAllSystemIDs (line 59) | func (sm *SystemManager) GetAllSystemIDs() []string {
method GetSystemData (line 71) | func (sm *SystemManager) GetSystemData(systemID string) *entities.Combin...
method GetSystemHostPort (line 81) | func (sm *SystemManager) GetSystemHostPort(systemID string) (string, str...
method SetSystemStatusInDB (line 92) | func (sm *SystemManager) SetSystemStatusInDB(systemID string, status str...
method RemoveAllSystems (line 113) | func (sm *SystemManager) RemoveAllSystems() {
method StopUpdater (line 120) | func (s *System) StopUpdater() {
method CreateRecords (line 124) | func (s *System) CreateRecords(data *entities.CombinedData) (*core.Recor...
FILE: internal/hub/transport/ssh.go
type SSHTransport (line 19) | type SSHTransport struct
method SetClient (line 53) | func (t *SSHTransport) SetClient(client *ssh.Client) {
method SetAgentVersion (line 58) | func (t *SSHTransport) SetAgentVersion(version semver.Version) {
method GetClient (line 63) | func (t *SSHTransport) GetClient() *ssh.Client {
method GetAgentVersion (line 68) | func (t *SSHTransport) GetAgentVersion() semver.Version {
method Request (line 73) | func (t *SSHTransport) Request(ctx context.Context, action common.WebS...
method IsConnected (line 123) | func (t *SSHTransport) IsConnected() bool {
method Close (line 128) | func (t *SSHTransport) Close() {
method connect (line 136) | func (t *SSHTransport) connect() error {
method createSessionWithTimeout (line 161) | func (t *SSHTransport) createSessionWithTimeout(ctx context.Context) (...
method RequestWithRetry (line 198) | func (t *SSHTransport) RequestWithRetry(ctx context.Context, action co...
type SSHTransportConfig (line 29) | type SSHTransportConfig struct
function NewSSHTransport (line 38) | func NewSSHTransport(cfg SSHTransportConfig) *SSHTransport {
function extractAgentVersion (line 192) | func extractAgentVersion(versionString string) (semver.Version, error) {
function isConnectionError (line 218) | func isConnectionError(err error) bool {
FILE: internal/hub/transport/transport.go
type Transport (line 19) | type Transport interface
function UnmarshalResponse (line 32) | func UnmarshalResponse(resp common.AgentResponse, action common.WebSocke...
function unmarshalLegacyResponse (line 48) | func unmarshalLegacyResponse(resp common.AgentResponse, action common.We...
FILE: internal/hub/transport/websocket.go
type WebSocketTransport (line 17) | type WebSocketTransport struct
method Request (line 27) | func (t *WebSocketTransport) Request(ctx context.Context, action commo...
method IsConnected (line 65) | func (t *WebSocketTransport) IsConnected() bool {
method Close (line 70) | func (t *WebSocketTransport) Close() {
function NewWebSocketTransport (line 22) | func NewWebSocketTransport(wsConn *ws.WsConn) *WebSocketTransport {
FILE: internal/hub/update.go
function Update (line 14) | func Update(cmd *cobra.Command, _ []string) {
function restartService (line 58) | func restartService() {
FILE: internal/hub/ws/handlers.go
type ResponseHandler (line 15) | type ResponseHandler interface
type BaseHandler (line 21) | type BaseHandler struct
method HandleLegacy (line 23) | func (h *BaseHandler) HandleLegacy(rawData []byte) error {
type fingerprintHandler (line 32) | type fingerprintHandler struct
method HandleLegacy (line 36) | func (h *fingerprintHandler) HandleLegacy(rawData []byte) error {
method Handle (line 40) | func (h *fingerprintHandler) Handle(agentResponse common.AgentResponse...
method GetFingerprint (line 49) | func (ws *WsConn) GetFingerprint(ctx context.Context, token string, sign...
FILE: internal/hub/ws/request_manager.go
type RequestID (line 16) | type RequestID
type PendingRequest (line 19) | type PendingRequest struct
type RequestManager (line 28) | type RequestManager struct
method SendRequest (line 45) | func (rm *RequestManager) SendRequest(ctx context.Context, action comm...
method sendMessage (line 89) | func (rm *RequestManager) sendMessage(data any) error {
method handleResponse (line 103) | func (rm *RequestManager) handleResponse(message *gws.Message) {
method routeLegacyResponse (line 139) | func (rm *RequestManager) routeLegacyResponse(message *gws.Message) {
method cleanupRequest (line 166) | func (rm *RequestManager) cleanupRequest(req *PendingRequest) {
method cancelRequest (line 172) | func (rm *RequestManager) cancelRequest(reqID RequestID) {
method deleteRequest (line 183) | func (rm *RequestManager) deleteRequest(reqID RequestID) {
method Close (line 190) | func (rm *RequestManager) Close() {
function NewRequestManager (line 36) | func NewRequestManager(conn *gws.Conn) *RequestManager {
FILE: internal/hub/ws/request_manager_test.go
function TestRequestManager_BasicFunctionality (line 14) | func TestRequestManager_BasicFunctionality(t *testing.T) {
FILE: internal/hub/ws/ws.go
constant deadline (line 19) | deadline = 70 * time.Second
type Handler (line 23) | type Handler struct
method OnOpen (line 66) | func (h *Handler) OnOpen(conn *gws.Conn) {
method OnMessage (line 71) | func (h *Handler) OnMessage(conn *gws.Conn, message *gws.Message) {
method OnClose (line 85) | func (h *Handler) OnClose(conn *gws.Conn, err error) {
type WsConn (line 28) | type WsConn struct
method Close (line 103) | func (ws *WsConn) Close(msg []byte) {
method Ping (line 113) | func (ws *WsConn) Ping() error {
method sendMessage (line 120) | func (ws *WsConn) sendMessage(data common.HubRequest[any]) error {
method handleAgentRequest (line 132) | func (ws *WsConn) handleAgentRequest(req *PendingRequest, handler Resp...
method IsConnected (line 162) | func (ws *WsConn) IsConnected() bool {
method AgentVersion (line 167) | func (ws *WsConn) AgentVersion() semver.Version {
method SendRequest (line 173) | func (ws *WsConn) SendRequest(ctx context.Context, action common.WebSo...
type FingerprintRecord (line 36) | type FingerprintRecord struct
function GetUpgrader (line 46) | func GetUpgrader() *gws.Upgrader {
function NewWsConnection (line 56) | func NewWsConnection(conn *gws.Conn, agentVersion semver.Version) *WsConn {
FILE: internal/hub/ws/ws_test.go
function TestGetUpgrader (line 20) | func TestGetUpgrader(t *testing.T) {
function TestNewWsConnection (line 37) | func TestNewWsConnection(t *testing.T) {
function TestWsConn_IsConnected (line 49) | func TestWsConn_IsConnected(t *testing.T) {
function TestWsConn_Close (line 56) | func TestWsConn_Close(t *testing.T) {
function TestWsConn_SendMessage_CBOR (line 66) | func TestWsConn_SendMessage_CBOR(t *testing.T) {
function TestWsConn_GetFingerprint_SignatureGeneration (line 91) | func TestWsConn_GetFingerprint_SignatureGeneration(t *testing.T) {
function TestWsConn_RequestSystemData_RequestFormat (line 141) | func TestWsConn_RequestSystemData_RequestFormat(t *testing.T) {
function TestFingerprintRecord (line 159) | func TestFingerprintRecord(t *testing.T) {
function TestDeadlineConstant (line 174) | func TestDeadlineConstant(t *testing.T) {
function TestCommonActions (line 179) | func TestCommonActions(t *testing.T) {
function TestFingerprintHandler (line 186) | func TestFingerprintHandler(t *testing.T) {
function TestHandler (line 201) | func TestHandler(t *testing.T) {
function TestWsConnChannelBehavior (line 210) | func TestWsConnChannelBehavior(t *testing.T) {
FILE: internal/hub/ws/ws_test_helpers.go
method GetPendingCount (line 6) | func (rm *RequestManager) GetPendingCount() int {
FILE: internal/migrations/0_collections_snapshot_0_19_0_dev_1.go
function init (line 8) | func init() {
FILE: internal/migrations/initial-settings.go
constant TempAdminEmail (line 11) | TempAdminEmail = "_@b.b"
function init (line 14) | func init() {
function GetEnv (line 65) | func GetEnv(key string) (value string, exists bool) {
FILE: internal/records/records.go
type RecordManager (line 19) | type RecordManager struct
method CreateLongerRecords (line 53) | func (rm *RecordManager) CreateLongerRecords() {
method AverageSystemStats (line 172) | func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records Re...
method AverageContainerStats (line 439) | func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records...
method DeleteOldRecords (line 492) | func (rm *RecordManager) DeleteOldRecords() {
type LongerRecordData (line 23) | type LongerRecordData struct
type RecordIds (line 30) | type RecordIds
function NewRecordManager (line 34) | func NewRecordManager(app core.App) *RecordManager {
type StatsRecord (line 38) | type StatsRecord struct
function deleteOldAlertsHistory (line 519) | func deleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeleti...
function deleteOldSystemStats (line 538) | func deleteOldSystemStats(app core.App) error {
function deleteOldSystemdServiceRecords (line 580) | func deleteOldSystemdServiceRecords(app core.App) error {
function deleteOldContainerRecords (line 594) | func deleteOldContainerRecords(app core.App) error {
function deleteOldQuietHours (line 608) | func deleteOldQuietHours(app core.App) error {
function twoDecimals (line 619) | func twoDecimals(value float64) float64 {
FILE: internal/records/records_test.go
function TestDeleteOldRecords (line 21) | func TestDeleteOldRecords(t *testing.T) {
function TestDeleteOldSystemStats (line 106) | func TestDeleteOldSystemStats(t *testing.T) {
function TestDeleteOldAlertsHistory (line 198) | func TestDeleteOldAlertsHistory(t *testing.T) {
function TestDeleteOldAlertsHistoryEdgeCases (line 304) | func TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {
function TestDeleteOldSystemdServiceRecords (line 354) | func TestDeleteOldSystemdServiceRecords(t *testing.T) {
function TestRecordManagerCreation (line 431) | func TestRecordManagerCreation(t *testing.T) {
function TestTwoDecimals (line 441) | func TestTwoDecimals(t *testing.T) {
FILE: internal/records/records_test_helpers.go
function DeleteOldSystemStats (line 10) | func DeleteOldSystemStats(app core.App) error {
function DeleteOldAlertsHistory (line 15) | func DeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeleti...
function TwoDecimals (line 20) | func TwoDecimals(value float64) float64 {
FILE: internal/site/src/components/add-system.tsx
function AddSystemButton (line 38) | function AddSystemButton({ className }: { className?: string }) {
function handleSubmit (line 104) | async function handleSubmit(e: SubmitEvent) {
type CopyButtonProps (line 287) | interface CopyButtonProps {
FILE: internal/site/src/components/alerts-history-columns.tsx
method cell (line 61) | cell({ row, getValue }) {
FILE: internal/site/src/components/alerts/alerts-sheet.tsx
function AlertContent (line 152) | function AlertContent({
FILE: internal/site/src/components/charts/area-chart.tsx
type DataPoint (line 16) | type DataPoint = {
function AreaChartDefault (line 24) | function AreaChartDefault({
FILE: internal/site/src/components/charts/hooks.ts
type ContainerChartConfigs (line 6) | interface ContainerChartConfigs {
function useContainerChartConfigs (line 17) | function useContainerChartConfigs(containerData: ChartData["containerDat...
function useYAxisWidth (line 86) | function useYAxisWidth() {
function useNetworkInterfaces (line 112) | function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
FILE: internal/site/src/components/charts/line-chart.tsx
type DataPoint (line 15) | type DataPoint = {
function LineChartDefault (line 21) | function LineChartDefault({
FILE: internal/site/src/components/containers-table/containers-table-columns.tsx
function getStatusValue (line 32) | function getStatusValue(status: string): number {
function HeaderButton (line 211) | function HeaderButton({
function getPortValue (line 241) | function getPortValue(ports: string | undefined): number {
FILE: internal/site/src/components/containers-table/containers-table.tsx
function ContainersTable (line 38) | function ContainersTable({ systemId }: { systemId?: string }) {
function getLogsHtml (line 275) | async function getLogsHtml(container: ContainerRecord): Promise<string> {
function getInfoHtml (line 291) | async function getInfoHtml(container: ContainerRecord): Promise<string> {
function ContainerSheet (line 310) | function ContainerSheet({
function ContainersTableHead (line 462) | function ContainersTableHead({ table }: { table: TableType<ContainerReco...
function LogsFullscreenDialog (line 512) | function LogsFullscreenDialog({
function InfoFullscreenDialog (line 564) | function InfoFullscreenDialog({
FILE: internal/site/src/components/copy-to-clipboard.tsx
function CopyToClipboard (line 7) | function CopyToClipboard({ content }: { content: string }) {
function CopyTextarea (line 25) | function CopyTextarea({ content }: { content: string }) {
FILE: internal/site/src/components/footer-repo-link.tsx
function FooterRepoLink (line 4) | function FooterRepoLink() {
FILE: internal/site/src/components/install-dropdowns.tsx
function copyDockerCompose (line 25) | function copyDockerCompose(port = "45876", publicKey: string, token: str...
function copyDockerRun (line 44) | function copyDockerRun(port = "45876", publicKey: string, token: string) {
function copyLinuxCommand (line 50) | function copyLinuxCommand(port = "45876", publicKey: string, token: stri...
function copyWindowsCommand (line 61) | function copyWindowsCommand(port = "45876", publicKey: string, token: st...
type DropdownItem (line 67) | interface DropdownItem {
FILE: internal/site/src/components/lang-toggle.tsx
function LangToggle (line 10) | function LangToggle() {
FILE: internal/site/src/components/login/auth-form.tsx
function UserAuthForm (line 57) | function UserAuthForm({
FILE: internal/site/src/components/login/forgot-pass-form.tsx
function ForgotPassword (line 21) | function ForgotPassword() {
FILE: internal/site/src/components/login/otp-forms.tsx
function OtpInputForm (line 14) | function OtpInputForm({ otpId, mfaId }: { otpId: string; mfaId: string }) {
function OtpRequestForm (line 45) | function OtpRequestForm() {
FILE: internal/site/src/components/logo.tsx
function Logo (line 5) | function Logo({ className }: { className?: string }) {
FILE: internal/site/src/components/mode-toggle.tsx
function ModeToggle (line 8) | function ModeToggle() {
FILE: internal/site/src/components/navbar.tsx
function Navbar (line 39) | function Navbar() {
function SearchButton (line 164) | function SearchButton() {
FILE: internal/site/src/components/router.tsx
function Link (line 41) | function Link(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
FILE: internal/site/src/components/routes/settings/alerts-history-data-table.tsx
function AlertsHistoryDataTable (line 61) | function AlertsHistoryDataTable() {
FILE: internal/site/src/components/routes/settings/config-yaml.tsx
function ConfigYaml (line 15) | function ConfigYaml() {
FILE: internal/site/src/components/routes/settings/general.tsx
function SettingsProfilePage (line 20) | function SettingsProfilePage({ userSettings }: { userSettings: UserSetti...
FILE: internal/site/src/components/routes/settings/heartbeat.tsx
type HeartbeatStatus (line 14) | interface HeartbeatStatus {
function HeartbeatSettings (line 22) | function HeartbeatSettings() {
function EnabledState (line 104) | function EnabledState({
function NotEnabledState (line 172) | function NotEnabledState({ isLoading }: { isLoading?: boolean }) {
function ConfigItem (line 200) | function ConfigItem({ label, value, mono }: { label: string; value: stri...
function EnvVarItem (line 209) | function EnvVarItem({ name, description, example }: { name: string; desc...
FILE: internal/site/src/components/routes/settings/layout.tsx
function saveSettings (line 37) | async function saveSettings(newSettings: Partial<UserSettings>) {
function SettingsLayout (line 65) | function SettingsLayout() {
function SettingsContent (line 146) | function SettingsContent({ name }: { name: string }) {
FILE: internal/site/src/components/routes/settings/notifications.tsx
type ShoutrrrUrlCardProps (line 19) | interface ShoutrrrUrlCardProps {
function addWebhook (line 41) | function addWebhook() {
function updateWebhook (line 51) | function updateWebhook(index: number, value: string) {
function updateSettings (line 57) | async function updateSettings() {
FILE: internal/site/src/components/routes/settings/quiet-hours.tsx
function QuietHours (line 47) | function QuietHours() {
function formatDateTimeLocal (line 276) | function formatDateTimeLocal(date: Date): string {
function QuietHoursDialog (line 285) | function QuietHoursDialog({
FILE: internal/site/src/components/routes/settings/sidebar-nav.tsx
type SidebarNavProps (line 10) | interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
function SidebarNav (line 21) | function SidebarNav({ className, items, ...props }: SidebarNavProps) {
FILE: internal/site/src/components/routes/settings/tokens-fingerprints.tsx
function sortFingerprints (line 48) | function sortFingerprints(fingerprints: FingerprintRecord[]) {
function updateToken (line 143) | async function updateToken(enable: number = -1, permanent: number = -1) {
function updateFingerprint (line 363) | async function updateFingerprint(fingerprint: FingerprintRecord, rotateT...
FILE: internal/site/src/components/routes/smart.tsx
function Smart (line 6) | function Smart() {
FILE: internal/site/src/components/routes/system.tsx
type ChartTimeData (line 68) | type ChartTimeData = {
function getTimeData (line 80) | function getTimeData(chartTime: ChartTimes, lastCreated: number) {
function addEmptyValues (line 101) | function addEmptyValues<T extends { created: string | number | null }>(
function getStats (line 128) | async function getStats<T extends SystemStatsRecord | ContainerStatsReco...
function dockerOrPodman (line 146) | function dockerOrPodman(str: string, isPodman: boolean): string {
method dataKey (line 592) | dataKey(data: SystemStatsRecord) {
method dataKey (line 603) | dataKey(data: SystemStatsRecord) {
function GpuEnginesChart (line 891) | function GpuEnginesChart({ chartData }: { chartData: ChartData }) {
function FilterBar (line 927) | function FilterBar({ store = $containerFilter }: { store?: typeof $conta...
function ChartCard (line 998) | function ChartCard({
function LazyContainersTable (line 1045) | function LazyContainersTable({ systemId }: { systemId: string }) {
function LazySmartTable (line 1056) | function LazySmartTable({ systemId }: { systemId: string }) {
function LazySystemdTable (line 1067) | function LazySystemdTable({ systemId }: { systemId: string }) {
FILE: internal/site/src/components/routes/system/info-bar.tsx
function InfoBar (line 25) | function InfoBar({
FILE: internal/site/src/components/routes/system/smart-table.tsx
function formatCapacity (line 101) | function formatCapacity(bytes: number): string {
constant SMART_DEVICE_FIELDS (line 106) | const SMART_DEVICE_FIELDS = "id,system,name,model,state,capacity,temp,ty...
function HeaderButton (line 262) | function HeaderButton({
function DisksTable (line 287) | function DisksTable({ systemId }: { systemId?: string }) {
function SmartTableHead (line 636) | function SmartTableHead({ table }: { table: TableType<SmartDeviceRecord>...
function DiskSheet (line 683) | function DiskSheet({
FILE: internal/site/src/components/systemd-table/systemd-table-columns.tsx
function getSubStateColor (line 19) | function getSubStateColor(subState: ServiceSubState) {
function HeaderButton (line 172) | function HeaderButton({ column, name, Icon }: { column: Column<SystemdRe...
function getStatusColor (line 187) | function getStatusColor(status: ServiceStatus) {
FILE: internal/site/src/components/systemd-table/systemd-table.tsx
function SystemdTable (line 32) | function SystemdTable({ systemId }: { systemId?: string }) {
function SystemdSheet (line 257) | function SystemdSheet({
function SystemdTableHead (line 614) | function SystemdTableHead({ table }: { table: TableType<SystemdRecord> }) {
FILE: internal/site/src/components/systems-table/systems-table-columns.tsx
constant STATUS_COLORS (line 76) | const STATUS_COLORS = {
function getMeterStateByThresholds (line 83) | function getMeterStateByThresholds(value: number, warn = 65, crit = 90):...
function SystemsTableColumns (line 91) | function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syst...
function sortableHeader (line 440) | function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
function TableCellWithMeter (line 458) | function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
function DiskCellWithMultiple (line 479) | function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
function IndicatorDot (line 572) | function IndicatorDot({ system, className }: { system: SystemRecord; cla...
FILE: internal/site/src/components/systems-table/systems-table.tsx
type ViewMode (line 53) | type ViewMode = "table" | "grid"
type StatusFilter (line 54) | type StatusFilter = "all" | SystemRecord["status"]
function SystemsTable (line 58) | function SystemsTable() {
function SystemsTableHead (line 390) | function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
FILE: internal/site/src/components/theme-provider.tsx
type Theme (line 3) | type Theme = "dark" | "light" | "system"
type ThemeProviderProps (line 5) | type ThemeProviderProps = {
type ThemeProviderState (line 11) | type ThemeProviderState = {
function ThemeProvider (line 23) | function ThemeProvider({
FILE: internal/site/src/components/ui/badge.tsx
type BadgeProps (line 26) | interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, Varia...
function Badge (line 28) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: internal/site/src/components/ui/button.tsx
type ButtonProps (line 33) | interface ButtonProps
FILE: internal/site/src/components/ui/chart.tsx
constant THEMES (line 11) | const THEMES = { light: "", dark: ".dark" } as const
type ChartConfig (line 13) | type ChartConfig = {
function getPayloadConfigFromPayload (line 378) | function getPayloadConfigFromPayload(config: ChartConfig, payload: unkno...
function pinnedAxisDomain (line 435) | function pinnedAxisDomain(): AxisDomain {
FILE: internal/site/src/components/ui/collapsible.tsx
type CollapsibleProps (line 6) | interface CollapsibleProps {
function Collapsible (line 15) | function Collapsible({ title, children, description, defaultOpen = false...
FILE: internal/site/src/components/ui/command.tsx
function Command (line 7) | function Command({ className, ...props }: React.ComponentProps<typeof Co...
function CommandDialog (line 17) | function CommandDialog({
function CommandInput (line 45) | function CommandInput({ className, ...props }: React.ComponentProps<type...
function CommandList (line 61) | function CommandList({ className, ...props }: React.ComponentProps<typeo...
function CommandEmpty (line 71) | function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandP...
function CommandGroup (line 75) | function CommandGroup({ className, ...props }: React.ComponentProps<type...
function CommandSeparator (line 88) | function CommandSeparator({ className, ...props }: React.ComponentProps<...
function CommandItem (line 98) | function CommandItem({ className, ...props }: React.ComponentProps<typeo...
function CommandShortcut (line 111) | function CommandShortcut({ className, ...props }: React.ComponentProps<"...
FILE: internal/site/src/components/ui/icons.tsx
function TuxIcon (line 4) | function TuxIcon(props: SVGProps<SVGSVGElement>) {
function WindowsIcon (line 16) | function WindowsIcon(props: SVGProps<SVGSVGElement>) {
function AppleIcon (line 30) | function AppleIcon(props: SVGProps<SVGSVGElement>) {
function FreeBsdIcon (line 42) | function FreeBsdIcon(props: SVGProps<SVGSVGElement>) {
function DockerIcon (line 54) | function DockerIcon(props: SVGProps<SVGSVGElement>) {
function Rows (line 64) | function Rows(props: SVGProps<SVGSVGElement>) {
function ChartAverage (line 76) | function ChartAverage(props: SVGProps<SVGSVGElement>) {
function ChartMax (line 87) | function ChartMax(props: SVGProps<SVGSVGElement>) {
function EthernetIcon (line 98) | function EthernetIcon(props: SVGProps<SVGSVGElement>) {
function ThermometerIcon (line 107) | function ThermometerIcon(props: SVGProps<SVGSVGElement>) {
function GpuIcon (line 116) | function GpuIcon(props: SVGProps<SVGSVGElement>) {
function HourglassIcon (line 126) | function HourglassIcon(props: SVGProps<SVGSVGElement>) {
function WebSocketIcon (line 135) | function WebSocketIcon(props: SVGProps<SVGSVGElement>) {
function BatteryMediumIcon (line 145) | function BatteryMediumIcon(props: SVGProps<SVGSVGElement>) {
function BatteryLowIcon (line 154) | function BatteryLowIcon(props: SVGProps<SVGSVGElement>) {
function BatteryHighIcon (line 163) | function BatteryHighIcon(props: SVGProps<SVGSVGElement>) {
function BatteryFullIcon (line 172) | function BatteryFullIcon(props: SVGProps<SVGSVGElement>) {
function PlugChargingIcon (line 181) | function PlugChargingIcon(props: SVGProps<SVGSVGElement>) {
function SquareArrowRightEnterIcon (line 190) | function SquareArrowRightEnterIcon(props: SVGProps<SVGSVGElement>) {
FILE: internal/site/src/components/ui/input-copy.tsx
function InputCopy (line 8) | function InputCopy({ value, id, name }: { value: string; id: string; nam...
FILE: internal/site/src/components/ui/input-tags.tsx
type InputTagsProps (line 8) | type InputTagsProps = Omit<InputProps, "value" | "onChange"> & {
FILE: internal/site/src/components/ui/input.tsx
function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...
FILE: internal/site/src/components/ui/otp.tsx
function InputOTP (line 7) | function InputOTP({
function InputOTPGroup (line 24) | function InputOTPGroup({ className, ...props }: React.ComponentProps<"di...
function InputOTPSlot (line 28) | function InputOTPSlot({
function InputOTPSeparator (line 58) | function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
FILE: internal/site/src/components/ui/sheet.tsx
function Sheet (line 7) | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive....
function SheetTrigger (line 11) | function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPri...
function SheetClose (line 15) | function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimi...
function SheetPortal (line 19) | function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrim...
function SheetOverlay (line 23) | function SheetOverlay({ className, ...props }: React.ComponentProps<type...
function SheetContent (line 36) | function SheetContent({
function SheetHeader (line 73) | function SheetHeader({ className, ...props }: React.ComponentProps<"div"...
function SheetFooter (line 77) | function SheetFooter({ className, ...props }: React.ComponentProps<"div"...
function SheetTitle (line 81) | function SheetTitle({ className, ...props }: React.ComponentProps<typeof...
function SheetDescription (line 91) | function SheetDescription({ className, ...props }: React.ComponentProps<...
FILE: internal/site/src/components/ui/textarea.tsx
type TextareaProps (line 5) | interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAre...
FILE: internal/site/src/components/ui/toast.tsx
type ToastProps (line 97) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement (line 99) | type ToastActionElement = React.ReactElement<typeof ToastAction>
FILE: internal/site/src/components/ui/toaster.tsx
function Toaster (line 4) | function Toaster() {
FILE: internal/site/src/components/ui/tooltip.tsx
function TooltipProvider (line 6) | function TooltipProvider({ delayDuration = 50, ...props }: React.Compone...
function Tooltip (line 10) | function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimit...
function TooltipTrigger (line 18) | function TooltipTrigger({ ...props }: React.ComponentProps<typeof Toolti...
function TooltipContent (line 22) | function TooltipContent({
FILE: internal/site/src/components/ui/use-toast.ts
constant TOAST_LIMIT (line 6) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 7) | const TOAST_REMOVE_DELAY = 1000000
type ToasterToast (line 9) | type ToasterToast = ToastProps & {
function genId (line 25) | function genId() {
type ActionType (line 30) | type ActionType = typeof actionTypes
type Action (line 32) | type Action =
type State (line 50) | interface State {
function dispatch (line 129) | function dispatch(action: Action) {
type Toast (line 136) | type Toast = Omit<ToasterToast, "id">
function toast (line 138) | function toast({ ...props }: Toast) {
function useToast (line 167) | function useToast() {
FILE: internal/site/src/lib/alerts.ts
function fetchAlerts (line 106) | async function fetchAlerts(): Promise<AlertRecord[]> {
function add (line 111) | function add(alerts: AlertRecord[]) {
function remove (line 121) | function remove(alerts: Pick<AlertRecord, "name" | "system">[]) {
function subscribe (line 161) | async function subscribe() {
function unsubscribe (line 165) | function unsubscribe() {
function refresh (line 169) | async function refresh() {
FILE: internal/site/src/lib/api.ts
function logOut (line 29) | function logOut() {
function updateUserSettings (line 40) | async function updateUserSettings() {
function getPbTimestamp (line 57) | function getPbTimestamp(timeString: ChartTimes, d?: Date) {
FILE: internal/site/src/lib/enums.ts
type Os (line 2) | enum Os {
type ChartType (line 10) | enum ChartType {
type Unit (line 18) | enum Unit {
type MeterState (line 26) | enum MeterState {
type SystemStatus (line 33) | enum SystemStatus {
type BatteryState (line 41) | enum BatteryState {
type HourFormat (line 51) | enum HourFormat {
type ContainerHealth (line 58) | enum ContainerHealth {
type ConnectionType (line 68) | enum ConnectionType {
type ServiceStatus (line 76) | enum ServiceStatus {
type ServiceSubState (line 88) | enum ServiceSubState {
FILE: internal/site/src/lib/i18n.ts
function activateLocale (line 13) | function activateLocale(locale: string, messages: Messages = enMessages) {
function dynamicActivate (line 22) | async function dynamicActivate(locale: string) {
function getLocale (line 36) | function getLocale() {
FILE: internal/site/src/lib/systemsManager.ts
constant COLLECTION (line 16) | const COLLECTION = pb.collection<SystemRecord>("systems")
constant FIELDS_DEFAULT (line 17) | const FIELDS_DEFAULT = "id,name,host,port,info,status"
constant MAX_SYSTEM_NAME_LENGTH (line 20) | const MAX_SYSTEM_NAME_LENGTH = 22
function init (line 27) | function init() {
function onSystemsChanged (line 76) | function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem...
function fetchSystems (line 91) | async function fetchSystems(): Promise<SystemRecord[]> {
function validateSystemInfo (line 101) | function validateSystemInfo(system: SystemRecord) {
function add (line 108) | function add(system: SystemRecord) {
function update (line 119) | function update(system: SystemRecord) {
function remove (line 134) | function remove(system: SystemRecord) {
function removeFromStore (line 143) | function removeFromStore(system: SystemRecord, store: PreinitializedMapS...
function subscribe (line 156) | async function subscribe() {
function refresh (line 167) | async function refresh() {
FILE: internal/site/src/lib/use-intersection-observer.ts
type State (line 6) | type State = {
type UseIntersectionObserverOptions (line 14) | type UseIntersectionObserverOptions = {
type IntersectionReturn (line 57) | type IntersectionReturn = {
function useIntersectionObserver (line 72) | function useIntersectionObserver({
FILE: internal/site/src/lib/utils.ts
function cn (line 12) | function cn(...inputs: ClassValue[]) {
function listen (line 17) | function listen<T extends Event = Event>(node: Node, event: string, hand...
function copyToClipboard (line 22) | async function copyToClipboard(content: string) {
function toFixedFloat (line 180) | function toFixedFloat(num: number, digits: number) {
function decimalString (line 186) | function decimalString(num: number, digits = 2) {
function getStorageValue (line 202) | function getStorageValue(key: string, defaultValue: unknown, storageInte...
function useBrowserStorage (line 208) | function useBrowserStorage<T>(key: string, defaultValue: T, storageInter...
function formatTemperature (line 221) | function formatTemperature(celsius: number, unit?: Unit): { value: numbe...
function formatBytes (line 239) | function formatBytes(
function formatDuration (line 330) | function formatDuration(
function compareSemVer (line 386) | function compareSemVer(a: SemVer, b: SemVer) {
function debounce (line 397) | function debounce<T extends (...args: any[]) => any>(func: T, wait: numb...
function runOnce (line 410) | function runOnce<T extends (...args: any[]) => any>(fn: T): T {
function getVisualStringWidth (line 426) | function getVisualStringWidth(str: string): number {
function secondsToString (line 450) | function secondsToString(seconds: number, unit: "hour" | "minute" | "day...
function secondsToUptimeString (line 464) | function secondsToUptimeString(seconds: number): string {
FILE: internal/site/src/types.d.ts
type FingerprintRecord (line 13) | interface FingerprintRecord extends RecordModel {
type SystemRecord (line 25) | interface SystemRecord extends RecordModel {
type SystemInfo (line 35) | interface SystemInfo {
type SystemStats (line 82) | interface SystemStats {
type GPUData (line 151) | interface GPUData {
type ExtraFsStats (line 168) | interface ExtraFsStats {
type ContainerStatsRecord (line 191) | interface ContainerStatsRecord extends RecordModel {
type ContainerStats (line 197) | interface ContainerStats {
type SystemStatsRecord (line 212) | interface SystemStatsRecord extends RecordModel {
type AlertRecord (line 218) | interface AlertRecord extends RecordModel {
type AlertsHistoryRecord (line 228) | interface AlertsHistoryRecord extends RecordModel {
type QuietHoursRecord (line 238) | interface QuietHoursRecord extends RecordModel {
type ContainerRecord (line 252) | interface ContainerRecord extends RecordModel {
type ChartTimes (line 266) | type ChartTimes = "1m" | "1h" | "12h" | "24h" | "1w" | "30d"
type ChartTimeData (line 268) | interface ChartTimeData {
type UserSettings (line 280) | interface UserSettings {
type ChartDataContainer (line 293) | type ChartDataContainer = {
type SemVer (line 299) | interface SemVer {
type ChartData (line 305) | interface ChartData {
type AlertInfo (line 315) | interface AlertInfo {
type AlertMap (line 329) | type AlertMap = Record<string, Map<string, AlertRecord>>
type SmartData (line 331) | interface SmartData {
type SmartAttribute (line 354) | interface SmartAttribute {
type SystemDetailsRecord (line 373) | interface SystemDetailsRecord extends RecordModel {
type SmartDeviceRecord (line 386) | interface SmartDeviceRecord extends RecordModel {
type SystemdRecord (line 403) | interface SystemdRecord extends RecordModel {
type SystemdServiceDetails (line 415) | interface SystemdServiceDetails {
FILE: internal/tests/api.go
type ApiScenario (line 27) | type ApiScenario struct
method Test (line 118) | func (scenario *ApiScenario) Test(t *testing.T) {
method Benchmark (line 148) | func (scenario *ApiScenario) Benchmark(b *testing.B) {
method normalizedName (line 156) | func (scenario *ApiScenario) normalizedName() string {
method test (line 166) | func (scenario *ApiScenario) test(t testing.TB) {
FILE: internal/tests/hub.go
type TestHub (line 20) | type TestHub struct
method Cleanup (line 100) | func (h *TestHub) Cleanup() {
function NewTestHub (line 29) | func NewTestHub(optTestDataDir ...string) (*TestHub, error) {
function NewTestHubWithConfig (line 49) | func NewTestHubWithConfig(config core.BaseAppConfig) (*TestHub, error) {
function CreateUser (line 67) | func CreateUser(app core.App, email string, password string) (*core.Reco...
function CreateRecord (line 81) | func CreateRecord(app core.App, collectionName string, fields map[string...
function ClearCollection (line 93) | func ClearCollection(t testing.TB, app core.App, collectionName string) ...
function CreateSystems (line 106) | func CreateSystems(app core.App, count int, userId string, status string...
function GetHubWithUser (line 129) | func GetHubWithUser(t *testing.T) (*TestHub, *core.Record) {
FILE: internal/users/users.go
type UserManager (line 14) | type UserManager struct
method InitializeUserRole (line 25) | func (um *UserManager) InitializeUserRole(e *core.RecordEvent) error {
method InitializeUserSettings (line 33) | func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) err...
method CreateFirstUser (line 61) | func (um *UserManager) CreateFirstUser(e *core.RequestEvent) error {
function NewUserManager (line 18) | func NewUserManager(app core.App) *UserManager {
Condensed preview — 357 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,945K chars).
[
{
"path": ".dockerignore",
"chars": 557,
"preview": "# Node.js dependencies\nnode_modules\ninternalsite/node_modules\n\n# Go build artifacts and binaries\nbuild\ndist\n*.exe\nbeszel"
},
{
"path": ".gitattributes",
"chars": 26,
"preview": "*.tsx linguist-language=Go"
},
{
"path": ".github/CODEOWNERS",
"chars": 54,
"preview": "# Everything needs to be reviewed by Hank\n* @henrygd"
},
{
"path": ".github/DISCUSSION_TEMPLATE/ideas.yml",
"chars": 426,
"preview": "body:\n - type: dropdown\n id: component\n attributes:\n label: Component\n description: Which part of Besze"
},
{
"path": ".github/DISCUSSION_TEMPLATE/support.yml",
"chars": 3284,
"preview": "body:\n - type: checkboxes\n id: terms\n attributes:\n label: Welcome!\n description: |\n Thank you fo"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 4699,
"preview": "name: 🐛 Bug report\ndescription: Use this template to report a bug or issue.\ntitle: '[Bug]: '\nlabels: ['bug']\nbody:\n - t"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 478,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: 🗣️ Translations\n url: https://crowdin.com/project/beszel\n abo"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 2023,
"preview": "name: 🚀 Feature request\ndescription: Request a new feature or change.\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbody:"
},
{
"path": ".github/funding.yml",
"chars": 25,
"preview": "buy_me_a_coffee: henrygd\n"
},
{
"path": ".github/pull_request_template.md",
"chars": 522,
"preview": "## 📃 Description\n\nA short description of the pull request changes should go here and the sections below should list in d"
},
{
"path": ".github/workflows/docker-images.yml",
"chars": 7549,
"preview": "name: Make docker images\n\non:\n push:\n tags:\n - \"v*\"\n\njobs:\n build:\n runs-on: ubuntu-latest\n strategy:\n "
},
{
"path": ".github/workflows/inactivity-actions.yml",
"chars": 1951,
"preview": "name: 'Issue and PR Maintenance'\r\n\r\non:\r\n schedule:\r\n - cron: '0 0 * * *' # runs at midnight UTC\r\n workflow_dispa"
},
{
"path": ".github/workflows/release.yml",
"chars": 1267,
"preview": "name: Make release and binaries\n\non:\n push:\n tags:\n - \"v*\"\n\npermissions:\n contents: write\n\njobs:\n goreleaser:"
},
{
"path": ".github/workflows/vulncheck.yml",
"chars": 759,
"preview": "# https://github.com/minio/minio/blob/master/.github/workflows/vulncheck.yml\n\nname: VulnCheck\non:\n pull_request:\n br"
},
{
"path": ".gitignore",
"chars": 276,
"preview": ".idea.md\npb_data\ndata\ntemp\n.vscode\nbeszel-agent\nbeszel_data\nbeszel_data*\ndist\n*.exe\ninternal/cmd/hub/hub\ninternal/cmd/ag"
},
{
"path": ".goreleaser.yml",
"chars": 7448,
"preview": "version: 2\n\nproject_name: beszel\n\nbefore:\n hooks:\n - go mod tidy\n - go generate -run fetchsmartctl ./agent\n\nbuild"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2024 henrygd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "Makefile",
"chars": 4395,
"preview": "# Default OS/ARCH values\nOS ?= $(shell go env GOOS)\nARCH ?= $(shell go env GOARCH)\n# Skip building the web UI if true\nSK"
},
{
"path": "SECURITY.md",
"chars": 284,
"preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf you find a vulnerability in the latest version, please [submit a pri"
},
{
"path": "agent/agent.go",
"chars": 8873,
"preview": "// Package agent implements the Beszel monitoring agent that collects and serves system metrics.\n//\n// The agent runs on"
},
{
"path": "agent/agent_cache.go",
"chars": 1365,
"preview": "package agent\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\ntype systemDataCache s"
},
{
"path": "agent/agent_cache_test.go",
"chars": 5809,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal"
},
{
"path": "agent/agent_test_helpers.go",
"chars": 227,
"preview": "//go:build testing\n\npackage agent\n\n// TESTING ONLY: GetConnectionManager is a helper function to get the connection mana"
},
{
"path": "agent/battery/battery.go",
"chars": 2472,
"preview": "//go:build !freebsd\n\n// Package battery provides functions to check if the system has a battery and to get the battery s"
},
{
"path": "agent/battery/battery_freebsd.go",
"chars": 189,
"preview": "//go:build freebsd\n\npackage battery\n\nimport \"errors\"\n\nfunc HasReadableBattery() bool {\n\treturn false\n}\n\nfunc GetBatteryS"
},
{
"path": "agent/client.go",
"chars": 9094,
"preview": "package agent\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings"
},
{
"path": "agent/client_test.go",
"chars": 14954,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"crypto/ed25519\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.c"
},
{
"path": "agent/connection_manager.go",
"chars": 6673,
"preview": "package agent\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/ag"
},
{
"path": "agent/connection_manager_test.go",
"chars": 9915,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"crypto/ed25519\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"githu"
},
{
"path": "agent/cpu.go",
"chars": 3912,
"preview": "package agent\n\nimport (\n\t\"math\"\n\t\"runtime\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/shirou/go"
},
{
"path": "agent/data_dir.go",
"chars": 2738,
"preview": "package agent\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/henrygd/beszel/agent/utils\"\n)\n\n"
},
{
"path": "agent/data_dir_test.go",
"chars": 7426,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/"
},
{
"path": "agent/deltatracker/deltatracker.go",
"chars": 2363,
"preview": "// Package deltatracker provides a tracker for calculating differences in numeric values over time.\npackage deltatracker"
},
{
"path": "agent/deltatracker/deltatracker_test.go",
"chars": 5533,
"preview": "package deltatracker\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc ExampleDeltaTracker() {\n"
},
{
"path": "agent/disk.go",
"chars": 22208,
"preview": "package agent\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/ag"
},
{
"path": "agent/disk_test.go",
"chars": 29088,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/en"
},
{
"path": "agent/docker.go",
"chars": 30350,
"preview": "package agent\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/sl"
},
{
"path": "agent/docker_test.go",
"chars": 50621,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/htt"
},
{
"path": "agent/emmc_common.go",
"chars": 1751,
"preview": "package agent\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc isEmmcBlockName(name string) bool {\n\tif !strings.HasPrefix("
},
{
"path": "agent/emmc_common_test.go",
"chars": 1853,
"preview": "package agent\n\nimport \"testing\"\n\nfunc TestParseHexOrDecByte(t *testing.T) {\n\ttests := []struct {\n\t\tin string\n\t\twant ui"
},
{
"path": "agent/emmc_linux.go",
"chars": 5059,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/henrygd/beszel/agen"
},
{
"path": "agent/emmc_linux_test.go",
"chars": 2547,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/entiti"
},
{
"path": "agent/emmc_stub.go",
"chars": 254,
"preview": "//go:build !linux\n\npackage agent\n\n// Non-Linux builds: eMMC health via sysfs is not available.\n\nfunc scanEmmcDevices() ["
},
{
"path": "agent/fingerprint.go",
"chars": 2666,
"preview": "package agent\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/shiro"
},
{
"path": "agent/fingerprint_test.go",
"chars": 2737,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"g"
},
{
"path": "agent/gpu.go",
"chars": 23953,
"preview": "package agent\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"runtime\"\n\t\""
},
{
"path": "agent/gpu_amd_linux.go",
"chars": 8936,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syn"
},
{
"path": "agent/gpu_amd_linux_test.go",
"chars": 7895,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\""
},
{
"path": "agent/gpu_amd_unsupported.go",
"chars": 196,
"preview": "//go:build !linux\n\npackage agent\n\nimport (\n\t\"errors\"\n)\n\nfunc (gm *GPUManager) hasAmdSysfs() bool {\n\treturn false\n}\n\nfunc"
},
{
"path": "agent/gpu_darwin.go",
"chars": 6768,
"preview": "//go:build darwin\n\npackage agent\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\""
},
{
"path": "agent/gpu_darwin_test.go",
"chars": 2737,
"preview": "//go:build darwin\n\npackage agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.co"
},
{
"path": "agent/gpu_darwin_unsupported.go",
"chars": 354,
"preview": "//go:build !darwin\n\npackage agent\n\n// startPowermetricsCollector is a no-op on non-darwin platforms; the real implementa"
},
{
"path": "agent/gpu_intel.go",
"chars": 5514,
"preview": "package agent\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"gith"
},
{
"path": "agent/gpu_nvml.go",
"chars": 6526,
"preview": "//go:build amd64 && (windows || (linux && glibc))\n\npackage agent\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\t\"unsaf"
},
{
"path": "agent/gpu_nvml_linux.go",
"chars": 1454,
"preview": "//go:build glibc && linux && amd64\n\npackage agent\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/"
},
{
"path": "agent/gpu_nvml_unsupported.go",
"chars": 275,
"preview": "//go:build (!linux && !windows) || !amd64 || (linux && !glibc)\n\npackage agent\n\nimport \"fmt\"\n\ntype nvmlCollector struct {"
},
{
"path": "agent/gpu_nvml_windows.go",
"chars": 462,
"preview": "//go:build windows && amd64\n\npackage agent\n\nimport (\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc openLibrary(name string) (uintp"
},
{
"path": "agent/gpu_nvtop.go",
"chars": 4101,
"preview": "package agent\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henryg"
},
{
"path": "agent/gpu_test.go",
"chars": 62858,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/he"
},
{
"path": "agent/handlers.go",
"chars": 6573,
"preview": "package agent\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/interna"
},
{
"path": "agent/handlers_test.go",
"chars": 2918,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/inte"
},
{
"path": "agent/health/health.go",
"chars": 1470,
"preview": "// Package health provides functions to check and update the health of the agent.\n// It uses a file in the temp director"
},
{
"path": "agent/health/health_test.go",
"chars": 1841,
"preview": "//go:build testing\n\npackage health\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"testing/synctest\"\n\n\t\"github.co"
},
{
"path": "agent/lhm/beszel_lhm.cs",
"chars": 2536,
"preview": "using System;\nusing System.Globalization;\nusing LibreHardwareMonitor.Hardware;\n\nclass Program\n{\n static void Main()\n {"
},
{
"path": "agent/lhm/beszel_lhm.csproj",
"chars": 432,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <OutputType>Exe</OutputType>\n <TargetFramework>net48</TargetF"
},
{
"path": "agent/mdraid_linux.go",
"chars": 6356,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/henrygd/besz"
},
{
"path": "agent/mdraid_linux_test.go",
"chars": 3529,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/entiti"
},
{
"path": "agent/mdraid_stub.go",
"chars": 195,
"preview": "//go:build !linux\n\npackage agent\n\nfunc scanMdraidDevices() []*DeviceInfo {\n\treturn nil\n}\n\nfunc (sm *SmartManager) collec"
},
{
"path": "agent/network.go",
"chars": 8503,
"preview": "package agent\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/deltatracker\"\n\t"
},
{
"path": "agent/network_test.go",
"chars": 16149,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/deltatracker\"\n\t\"github"
},
{
"path": "agent/response.go",
"chars": 968,
"preview": "package agent\n\nimport (\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"github.com/henryg"
},
{
"path": "agent/sensors.go",
"chars": 5740,
"preview": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github"
},
{
"path": "agent/sensors_default.go",
"chars": 142,
"preview": "//go:build !windows\n\npackage agent\n\nimport (\n\t\"github.com/shirou/gopsutil/v4/sensors\"\n)\n\nvar getSensorTemps = sensors.Te"
},
{
"path": "agent/sensors_test.go",
"chars": 16216,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/ent"
},
{
"path": "agent/sensors_windows.go",
"chars": 6381,
"preview": "//go:build windows\n\n//go:generate dotnet build -c Release lhm/beszel_lhm.csproj\n\npackage agent\n\nimport (\n\t\"bufio\"\n\t\"cont"
},
{
"path": "agent/server.go",
"chars": 8937,
"preview": "package agent\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.co"
},
{
"path": "agent/server_test.go",
"chars": 17827,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"context\"\n\t\"crypto/ed25519\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/fil"
},
{
"path": "agent/smart.go",
"chars": 37844,
"preview": "//go:generate -command fetchsmartctl go run ./tools/fetchsmartctl\n//go:generate fetchsmartctl -out ./smartmontools/smart"
},
{
"path": "agent/smart_nonwindows.go",
"chars": 137,
"preview": "//go:build !windows\n\npackage agent\n\nimport \"errors\"\n\nfunc ensureEmbeddedSmartctl() (string, error) {\n\treturn \"\", errors."
},
{
"path": "agent/smart_test.go",
"chars": 32857,
"preview": "//go:build testing\n\npackage agent\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/int"
},
{
"path": "agent/smart_windows.go",
"chars": 807,
"preview": "//go:build windows\n\npackage agent\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n)\n\n//go:embed smartmontools"
},
{
"path": "agent/system.go",
"chars": 8122,
"preview": "package agent\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/"
},
{
"path": "agent/systemd.go",
"chars": 8619,
"preview": "//go:build linux\n\npackage agent\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"math\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t"
},
{
"path": "agent/systemd_nonlinux.go",
"chars": 927,
"preview": "//go:build !linux\n\npackage agent\n\nimport (\n\t\"errors\"\n\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\n// syste"
},
{
"path": "agent/systemd_nonlinux_test.go",
"chars": 1337,
"preview": "//go:build !linux && testing\n\npackage agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewS"
},
{
"path": "agent/systemd_test.go",
"chars": 5796,
"preview": "//go:build linux && testing\n\npackage agent\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n"
},
{
"path": "agent/test-data/amdgpu.ids",
"chars": 23632,
"preview": "# List of AMDGPU IDs\n#\n# Syntax:\n# device_id,\trevision_id,\tproduct_name <-- single tab after comma\n\n1.0.0\n1114,\tC"
},
{
"path": "agent/test-data/container.json",
"chars": 353,
"preview": "{\n\t\"cpu_stats\": {\n\t\t\"cpu_usage\": {\n\t\t\t\"total_usage\": 312055276000\n\t\t},\n\t\t\"system_cpu_usage\": 1366399830000000\n\t},\n\t\"memo"
},
{
"path": "agent/test-data/container2.json",
"chars": 353,
"preview": "{\n\t\"cpu_stats\": {\n\t\t\"cpu_usage\": {\n\t\t\t\"total_usage\": 314891801000\n\t\t},\n\t\t\"system_cpu_usage\": 1368474900000000\n\t},\n\t\"memo"
},
{
"path": "agent/test-data/nvtop.json",
"chars": 735,
"preview": "[\n {\n \"device_name\": \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\",\n \"gpu_clock\": \"1485MHz\",\n \"mem_clock\": \"6001MHz\",\n "
},
{
"path": "agent/test-data/smart/nvme0.json",
"chars": 6327,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 5\n ],\n \"pre_release"
},
{
"path": "agent/test-data/smart/scan.json",
"chars": 601,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 5\n ],\n \"pre_release"
},
{
"path": "agent/test-data/smart/scsi.json",
"chars": 3229,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 3\n ],"
},
{
"path": "agent/test-data/smart/sda.json",
"chars": 24016,
"preview": "{\n \"json_format_version\": [\n 1,\n 0\n ],\n \"smartctl\": {\n \"version\": [\n 7,\n 5\n ],\n \"pre_release"
},
{
"path": "agent/test-data/system_info.json",
"chars": 434,
"preview": "{\n \"ID\": \"7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS\",\n \"Containers\": 14,\n \"ContainersRunning\": 3,\n "
},
{
"path": "agent/tools/fetchsmartctl/main.go",
"chars": 3296,
"preview": "package main\n\nimport (\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"flag\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\""
},
{
"path": "agent/update.go",
"chars": 3906,
"preview": "package agent\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\n\t\"github.com/henrygd/beszel/internal/ghupdate\"\n)\n\n// restart"
},
{
"path": "agent/utils/utils.go",
"chars": 2310,
"preview": "package utils\n\nimport (\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// GetEnv retrieves an environment variable with a "
},
{
"path": "agent/utils/utils_test.go",
"chars": 3820,
"preview": "package utils\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTwoDecimals"
},
{
"path": "agent/zfs/zfs_freebsd.go",
"chars": 163,
"preview": "//go:build freebsd\n\npackage zfs\n\nimport (\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc ARCSize() (uint64, error) {\n\treturn unix.Sysc"
},
{
"path": "agent/zfs/zfs_linux.go",
"chars": 658,
"preview": "//go:build linux\n\n// Package zfs provides functions to read ZFS statistics.\npackage zfs\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n"
},
{
"path": "agent/zfs/zfs_unsupported.go",
"chars": 129,
"preview": "//go:build !linux && !freebsd\n\npackage zfs\n\nimport \"errors\"\n\nfunc ARCSize() (uint64, error) {\n\treturn 0, errors.ErrUnsup"
},
{
"path": "beszel.go",
"chars": 598,
"preview": "// Package beszel provides core application constants and version information\n// which are used throughout the applicati"
},
{
"path": "go.mod",
"chars": 2825,
"preview": "module github.com/henrygd/beszel\n\ngo 1.26.1\n\nrequire (\n\tgithub.com/blang/semver v3.5.1+incompatible\n\tgithub.com/coreos/g"
},
{
"path": "go.sum",
"chars": 17967,
"preview": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:"
},
{
"path": "i18n.yml",
"chars": 135,
"preview": "files:\n - source: /internal/site/src/locales/en/\n translation: /internal/site/src/locales/%two_letters_code%/%two_le"
},
{
"path": "internal/alerts/alerts.go",
"chars": 9551,
"preview": "// Package alerts handles alert management and delivery.\npackage alerts\n\nimport (\n\t\"fmt\"\n\t\"net/mail\"\n\t\"net/url\"\n\t\"sync\"\n"
},
{
"path": "internal/alerts/alerts_api.go",
"chars": 3111,
"preview": "package alerts\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/poc"
},
{
"path": "internal/alerts/alerts_battery_test.go",
"chars": 12171,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/inter"
},
{
"path": "internal/alerts/alerts_cache.go",
"chars": 5301,
"preview": "package alerts\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/p"
},
{
"path": "internal/alerts/alerts_cache_test.go",
"chars": 6390,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/alerts\"\n\tbeszelTests "
},
{
"path": "internal/alerts/alerts_disk_test.go",
"chars": 4428,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/inter"
},
{
"path": "internal/alerts/alerts_history.go",
"chars": 2475,
"preview": "package alerts\n\nimport (\n\t\"time\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// On trigger"
},
{
"path": "internal/alerts/alerts_quiet_hours_test.go",
"chars": 12478,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/in"
},
{
"path": "internal/alerts/alerts_smart.go",
"chars": 2699,
"preview": "package alerts\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// handleSmartDeviceAlert sends "
},
{
"path": "internal/alerts/alerts_smart_test.go",
"chars": 8191,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\tbeszelTests \"github.com/henrygd/beszel/internal/t"
},
{
"path": "internal/alerts/alerts_status.go",
"chars": 6548,
"preview": "package alerts\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype alertInfo struct {\n"
},
{
"path": "internal/alerts/alerts_status_test.go",
"chars": 34800,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/in"
},
{
"path": "internal/alerts/alerts_system.go",
"chars": 9894,
"preview": "package alerts\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/syste"
},
{
"path": "internal/alerts/alerts_system_test.go",
"chars": 8334,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/in"
},
{
"path": "internal/alerts/alerts_test.go",
"chars": 18468,
"preview": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"te"
},
{
"path": "internal/alerts/alerts_test_helpers.go",
"chars": 2327,
"preview": "//go:build testing\n\npackage alerts\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\nfunc NewTestAl"
},
{
"path": "internal/cmd/agent/agent.go",
"chars": 5521,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/henrygd/beszel\"\n\t\"github.com/henrygd/beszel/agent\"\n\t"
},
{
"path": "internal/cmd/agent/agent_test.go",
"chars": 6658,
"preview": "package main\n\nimport (\n\t\"crypto/ed25519\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/agent\"\n\n\t\"github"
},
{
"path": "internal/cmd/hub/hub.go",
"chars": 2449,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel\"\n\t\"github.com/henrygd/beszel"
},
{
"path": "internal/common/common-ssh.go",
"chars": 269,
"preview": "package common\n\nvar (\n\t// Allowed ssh key exchanges\n\tDefaultKeyExchanges = []string{\"curve25519-sha256\"}\n\t// Allowed ssh"
},
{
"path": "internal/common/common-ws.go",
"chars": 2577,
"preview": "package common\n\nimport (\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\t\"github.c"
},
{
"path": "internal/dockerfile_agent",
"chars": 862,
"preview": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n"
},
{
"path": "internal/dockerfile_agent_alpine",
"chars": 792,
"preview": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n"
},
{
"path": "internal/dockerfile_agent_intel",
"chars": 708,
"preview": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n"
},
{
"path": "internal/dockerfile_agent_nvidia",
"chars": 1542,
"preview": "FROM --platform=$BUILDPLATFORM golang:bookworm AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download"
},
{
"path": "internal/dockerfile_hub",
"chars": 708,
"preview": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\n# Download Go modules\nCOPY ../go.mod ../go.sum ./"
},
{
"path": "internal/entities/container/container.go",
"chars": 4863,
"preview": "package container\n\nimport \"time\"\n\n// Docker container info from /containers/json\ntype ApiInfo struct {\n\tId string\n\t"
},
{
"path": "internal/entities/smart/smart.go",
"chars": 19636,
"preview": "package smart\n\nimport (\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Common types\ntype VersionInfo [2]int\n\ntype Smartctl"
},
{
"path": "internal/entities/smart/smart_test.go",
"chars": 1648,
"preview": "package smart\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSmartRawValueUnma"
},
{
"path": "internal/entities/system/system.go",
"chars": 9421,
"preview": "package system\n\n// TODO: this is confusing, make common package with common/types common/helpers etc\n\nimport (\n\t\"encodin"
},
{
"path": "internal/entities/systemd/systemd.go",
"chars": 3065,
"preview": "package systemd\n\nimport (\n\t\"math\"\n\t\"runtime\"\n\t\"time\"\n)\n\n// ServiceState represents the status of a systemd service\ntype "
},
{
"path": "internal/entities/systemd/systemd_test.go",
"chars": 3662,
"preview": "//go:build testing\n\npackage systemd_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/sys"
},
{
"path": "internal/ghupdate/extract.go",
"chars": 2980,
"preview": "package ghupdate\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\""
},
{
"path": "internal/ghupdate/ghupdate.go",
"chars": 9856,
"preview": "// Package ghupdate implements a new command to self update the current\n// executable with the latest GitHub release. Th"
},
{
"path": "internal/ghupdate/ghupdate_test.go",
"chars": 1058,
"preview": "package ghupdate\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestReleaseFindAssetBySuffix(t *testing.T) {\n\tr := releas"
},
{
"path": "internal/ghupdate/release.go",
"chars": 922,
"preview": "package ghupdate\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\ntype releaseAsset struct {\n\tName string `json:\"name\"`\n\tDownloa"
},
{
"path": "internal/ghupdate/selinux.go",
"chars": 1779,
"preview": "package ghupdate\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n// HandleSELinuxContext restores or applies the correct SELin"
},
{
"path": "internal/ghupdate/selinux_test.go",
"chars": 1683,
"preview": "package ghupdate\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestHandleSELinuxContext_NoSELinux(t *te"
},
{
"path": "internal/hub/agent_connect.go",
"chars": 11289,
"preview": "package hub\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/in"
},
{
"path": "internal/hub/agent_connect_test.go",
"chars": 54075,
"preview": "//go:build testing\n\npackage hub\n\nimport (\n\t\"crypto/ed25519\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepat"
},
{
"path": "internal/hub/config/config.go",
"chars": 8318,
"preview": "// Package config provides functions for syncing systems with the config.yml file\npackage config\n\nimport (\n\t\"fmt\"\n\t\"log\""
},
{
"path": "internal/hub/config/config_test.go",
"chars": 8141,
"preview": "//go:build testing\n\npackage config_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/interna"
},
{
"path": "internal/hub/expirymap/expirymap.go",
"chars": 2856,
"preview": "// Package expirymap provides a thread-safe map with expiring entries.\n// It supports TTL-based expiration with both laz"
},
{
"path": "internal/hub/expirymap/expirymap_test.go",
"chars": 14182,
"preview": "//go:build testing\n\npackage expirymap\n\nimport (\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/as"
},
{
"path": "internal/hub/heartbeat/heartbeat.go",
"chars": 8042,
"preview": "// Package heartbeat sends periodic outbound pings to an external monitoring\n// endpoint (e.g. BetterStack, Uptime Kuma,"
},
{
"path": "internal/hub/heartbeat/heartbeat_test.go",
"chars": 7471,
"preview": "//go:build testing\n\npackage heartbeat_test\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n"
},
{
"path": "internal/hub/hub.go",
"chars": 20011,
"preview": "// Package hub handles updating systems and serving the web UI.\npackage hub\n\nimport (\n\t\"crypto/ed25519\"\n\t\"encoding/pem\"\n"
},
{
"path": "internal/hub/hub_test.go",
"chars": 30602,
"preview": "//go:build testing\n\npackage hub_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/ed25519\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"io\"\n\t\"net/h"
},
{
"path": "internal/hub/hub_test_helpers.go",
"chars": 439,
"preview": "//go:build testing\n\npackage hub\n\nimport \"github.com/henrygd/beszel/internal/hub/systems\"\n\n// TESTING ONLY: GetSystemMana"
},
{
"path": "internal/hub/server_development.go",
"chars": 2055,
"preview": "//go:build development\n\npackage hub\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"str"
},
{
"path": "internal/hub/server_production.go",
"chars": 1492,
"preview": "//go:build !development\n\npackage hub\n\nimport (\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/henrygd/beszel\"\n"
},
{
"path": "internal/hub/systems/system.go",
"chars": 25437,
"preview": "package systems\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/fnv\"\n\t\"math/rand\"\n\t\"net\"\n\t\"strings\"\n\t\"sync"
},
{
"path": "internal/hub/systems/system_manager.go",
"chars": 12596,
"preview": "package systems\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/hub/ws\"\n\n\t\"github.com/henrygd/b"
},
{
"path": "internal/hub/systems/system_realtime.go",
"chars": 5545,
"preview": "package systems\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"gi"
},
{
"path": "internal/hub/systems/system_smart.go",
"chars": 2663,
"preview": "package systems\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\t\"g"
},
{
"path": "internal/hub/systems/system_systemd_test.go",
"chars": 2099,
"preview": "//go:build testing\n\npackage systems\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetSystemdSe"
},
{
"path": "internal/hub/systems/system_test.go",
"chars": 4827,
"preview": "//go:build testing\n\npackage systems\n\nimport (\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\nfunc "
},
{
"path": "internal/hub/systems/systems_production.go",
"chars": 383,
"preview": "//go:build !testing\n\npackage systems\n\n// Background SMART fetching is enabled in production but disabled for tests (syst"
},
{
"path": "internal/hub/systems/systems_test.go",
"chars": 13479,
"preview": "//go:build testing\n\npackage systems_test\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/h"
},
{
"path": "internal/hub/systems/systems_test_helpers.go",
"chars": 3664,
"preview": "//go:build testing\n\npackage systems\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tentities \"github.com/henrygd/beszel/internal/entities/"
},
{
"path": "internal/hub/transport/ssh.go",
"chars": 5427,
"preview": "package transport\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver\"\n\t\"gi"
},
{
"path": "internal/hub/transport/transport.go",
"chars": 3567,
"preview": "// Package transport provides a unified abstraction for hub-agent communication\n// over different transports (WebSocket,"
},
{
"path": "internal/hub/transport/websocket.go",
"chars": 2041,
"preview": "package transport\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel\"\n\t\"github."
},
{
"path": "internal/hub/update.go",
"chars": 2915,
"preview": "package hub\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/henrygd/beszel/internal/ghupdate\"\n\t\"github.com/spf13/"
},
{
"path": "internal/hub/ws/handlers.go",
"chars": 2226,
"preview": "package ws\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/internal/common\"\n"
},
{
"path": "internal/hub/ws/request_manager.go",
"chars": 4970,
"preview": "package ws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henr"
},
{
"path": "internal/hub/ws/request_manager_test.go",
"chars": 1893,
"preview": "//go:build testing\n\npackage ws\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// Test"
},
{
"path": "internal/hub/ws/ws.go",
"chars": 4917,
"preview": "package ws\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\t\"weak\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/henrygd/beszel\"\n\n\t\"gi"
},
{
"path": "internal/hub/ws/ws_test.go",
"chars": 8282,
"preview": "//go:build testing\n\npackage ws\n\nimport (\n\t\"crypto/ed25519\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/h"
},
{
"path": "internal/hub/ws/ws_test_helpers.go",
"chars": 219,
"preview": "//go:build testing\n\npackage ws\n\n// GetPendingCount returns the number of pending requests (for monitoring)\nfunc (rm *Req"
},
{
"path": "internal/migrations/0_collections_snapshot_0_19_0_dev_1.go",
"chars": 37871,
"preview": "package migrations\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n)"
},
{
"path": "internal/migrations/initial-settings.go",
"chars": 1696,
"preview": "package migrations\n\nimport (\n\t\"os\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrat"
},
{
"path": "internal/records/records.go",
"chars": 20431,
"preview": "// Package records handles creating longer records and deleting old records.\npackage records\n\nimport (\n\t\"encoding/json\"\n"
},
{
"path": "internal/records/records_test.go",
"chars": 14902,
"preview": "//go:build testing\n\npackage records_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/recor"
},
{
"path": "internal/records/records_test_helpers.go",
"chars": 588,
"preview": "//go:build testing\n\npackage records\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// DeleteOldSystemStats expose"
},
{
"path": "internal/site/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "internal/site/.prettierrc",
"chars": 121,
"preview": "{\n\t\"trailingComma\": \"es5\",\n\t\"useTabs\": true,\n\t\"tabWidth\": 2,\n\t\"semi\": false,\n\t\"singleQuote\": false,\n\t\"printWidth\": 120\n}"
},
{
"path": "internal/site/biome.json",
"chars": 1849,
"preview": "{\n\t\"$schema\": \"https://biomejs.dev/schemas/2.2.4/schema.json\",\n\t\"vcs\": {\n\t\t\"enabled\": true,\n\t\t\"clientKind\": \"git\",\n\t\t\"us"
},
{
"path": "internal/site/components.json",
"chars": 318,
"preview": "{\n\t\"$schema\": \"https://ui.shadcn.com/schema.json\",\n\t\"style\": \"default\",\n\t\"rsc\": false,\n\t\"tsx\": true,\n\t\"tailwind\": {\n\t\t\"c"
},
{
"path": "internal/site/embed.go",
"chars": 268,
"preview": "// Package site handles the Beszel frontend embedding.\npackage site\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n)\n\n//go:embed all:dist\nv"
},
{
"path": "internal/site/index.html",
"chars": 686,
"preview": "<!doctype html>\n<html lang=\"en\" dir=\"ltr\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<link rel=\"manifest\" href=\"./static/mani"
},
{
"path": "internal/site/lingui.config.ts",
"chars": 528,
"preview": "import { defineConfig } from \"@lingui/cli\"\n\nexport default defineConfig({\n\tlocales: [\n\t\t\"en\",\n\t\t\"ar\",\n\t\t\"bg\",\n\t\t\"cs\",\n\t\t"
},
{
"path": "internal/site/package.json",
"chars": 2396,
"preview": "{\n\t\"name\": \"beszel\",\n\t\"private\": true,\n\t\"version\": \"0.18.4\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite --host\",\n\t\t"
},
{
"path": "internal/site/public/static/manifest.json",
"chars": 221,
"preview": "{\n\t\"name\": \"Beszel\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"icon.png\",\n\t\t\t\"sizes\": \"512x512\",\n\t\t\t\"type\": \"image/png\"\n\t\t}\n\t],\n \"star"
},
{
"path": "internal/site/src/components/active-alerts.tsx",
"chars": 3002,
"preview": "import { alertInfo } from \"@/lib/alerts\"\nimport { $alerts, $allSystemsById } from \"@/lib/stores\"\nimport type { AlertReco"
},
{
"path": "internal/site/src/components/add-system.tsx",
"chars": 10652,
"preview": "import { msg, t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanos"
},
{
"path": "internal/site/src/components/alerts/alert-button.tsx",
"chars": 1239,
"preview": "import { t } from \"@lingui/core/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { BellIcon } from \"lucide-rea"
},
{
"path": "internal/site/src/components/alerts/alerts-sheet.tsx",
"chars": 10587,
"preview": "import { t } from \"@lingui/core/macro\"\nimport { Plural, Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@na"
},
{
"path": "internal/site/src/components/alerts-history-columns.tsx",
"chars": 5177,
"preview": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport type { ColumnDef } from \"@tans"
},
{
"path": "internal/site/src/components/charts/area-chart.tsx",
"chars": 3551,
"preview": "import { useMemo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YAxis } from \"recharts\"\nimport {\n\tChartContaine"
},
{
"path": "internal/site/src/components/charts/chart-time-select.tsx",
"chars": 1353,
"preview": "import { useStore } from \"@nanostores/react\"\nimport { HistoryIcon } from \"lucide-react\"\nimport { Select, SelectContent, "
},
{
"path": "internal/site/src/components/charts/container-chart.tsx",
"chars": 6712,
"preview": "// import Spinner from '../spinner'\nimport { useStore } from \"@nanostores/react\"\nimport { memo, useMemo } from \"react\"\ni"
},
{
"path": "internal/site/src/components/charts/disk-chart.tsx",
"chars": 2437,
"preview": "import { useLingui } from \"@lingui/react/macro\"\nimport { memo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YA"
},
{
"path": "internal/site/src/components/charts/gpu-power-chart.tsx",
"chars": 3439,
"preview": "import { memo, useMemo } from \"react\"\nimport { CartesianGrid, Line, LineChart, YAxis } from \"recharts\"\nimport {\n\tChartCo"
}
]
// ... and 157 more files (download for full content)
About this extraction
This page contains the full source code of the henrygd/beszel GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 357 files (3.4 MB), approximately 917.0k tokens, and a symbol index with 1605 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.